Value objects
Préférer un value object à une entity dès que possible. Il est plus simple à créer, à tester, à utiliser et à maintenir. Ces règles disent quand en créer un, et traitent de l’immuabilité, du comportement et de la persistance.
En pratique
Un montant n’est pas un nombre : il a une devise, et deux montants de devises différentes ne s’additionnent pas.
ts
const SUPPORTED_CURRENCIES: ReadonlySet<string> = new Set(['EUR']);
export class Currency {
readonly code: string;
private constructor(code: string) {
this.code = code;
}
static of(code: string): Result<Currency, UnsupportedCurrency> {
if (!SUPPORTED_CURRENCIES.has(code)) {
return err(new UnsupportedCurrency(code));
}
return ok(new Currency(code));
}
equals(other: Currency): boolean {
return other.code === this.code;
}
}ts
export class Money {
readonly minor: bigint;
readonly currency: Currency;
private constructor(minor: bigint, currency: Currency) {
this.minor = minor;
this.currency = currency;
}
static of(minor: bigint, currency: Currency): Result<Money, InvalidMoney> {
if (minor < 0n) {
return err(new InvalidMoney(minor));
}
return ok(new Money(minor, currency));
}
static zero(currency: Currency): Money {
return new Money(0n, currency);
}
sameCurrencyAs(other: Money): boolean {
return other.currency.equals(this.currency);
}
isLessThan(other: Money): boolean {
this.#assertSameCurrency(other);
return this.minor < other.minor;
}
plus(other: Money): Money {
this.#assertSameCurrency(other);
return new Money(this.minor + other.minor, this.currency);
}
minus(other: Money): Money {
if (this.isLessThan(other)) {
throw new Error(`Cannot subtract ${other.minor} from ${this.minor}`);
}
return new Money(this.minor - other.minor, this.currency);
}
equals(other: Money): boolean {
return other.minor === this.minor && other.currency.equals(this.currency);
}
toJSON(): { readonly minor: string; readonly currency: string } {
return { minor: this.minor.toString(), currency: this.currency.code };
}
#assertSameCurrency(other: Money): void {
if (!this.sameCurrencyAs(other)) {
throw new Error(`Cannot combine ${this.currency.code} and ${other.currency.code}`);
}
}
}- Un concept du domaine a son propre type, au lieu d’un
bigintet d’unstringpassés côte à côte. La devise aussi :Currencyn’accepte que les devises de la liste, etMoneyn’a plus à le vérifier (VO-01, VO-02, VO-08). - Immuable et complet dès sa naissance : le constructeur est privé,
ofrefuse un montant invalide, etplusrenvoie un nouveauMoney(VO-03, VO-04). - Aucun effet de bord, et une égalité par valeur (VO-05, VO-06).
Règles
Choisir un value object
| Code | Règle | Sévérité | Détection |
|---|---|---|---|
| VO-01 | Un concept du domaine n’est pas un type primitif | mineure | |
| VO-02 | Des attributs qui vont ensemble forment un tout | majeure |
Immuabilité et comportement
| Code | Règle | Sévérité | Détection |
|---|---|---|---|
| VO-03 | Un value object est immuable et naît complet | majeure | |
| VO-04 | Changer une valeur, c’est la remplacer | majeure | |
| VO-05 | Les méthodes d’un value object n’ont pas d’effet de bord et parlent le langage du métier | mineure | |
| VO-06 | Deux valeurs sont égales quand leurs attributs le sont | mineure |
Intégration et types standard
| Code | Règle | Sévérité | Détection |
|---|---|---|---|
| VO-07 | Ce qui vient d’un autre bounded context entre comme un value object | question | |
| VO-08 | Une valeur prise dans une liste connue a un type fermé | majeure |
Tests
| Code | Règle | Sévérité | Détection |
|---|---|---|---|
| VO-09 | Les tests prouvent l’immuabilité | mineure |
Persistance
| Code | Règle | Sévérité | Détection |
|---|---|---|---|
| VO-10 | Le modèle de données sert le modèle du domaine | majeure | |
| VO-11 | Remplacer une collection de valeurs ne laisse pas d’orphelins | majeure |