Les trois briques du Domain Driven Design
Bootcode IWA-S04 — Semaine 17, Jour 2
L'étudiant doit savoir :
đź”’
1. Implémenter un Value Object
Immutable avec validation
🪪
2. Implémenter une Entity
Avec identité et mutation
đź§
3. Choisir entre les deux
Savoir expliquer quand utiliser l'un ou l'autre
Rappel rapide du J1
Le vocabulaire DDD qu'on a découvert
Entity = identité
User change de nom mais reste le mĂŞme User
Value Object = attributs
100 EUR = 100 EUR, pas d'identité
Aggregate = frontière de cohérence
Order protège ses OrderItems
Exercice live
Classifier des concepts réels en Entity / VO / Aggregate
Module 1
Le vocabulaire DDD qu'on a découvert hier
L'architecture en couches nous a appris à séparer la logique métier du reste
đź§ Le Domain
Le cœur : la logique métier pure, sans dépendance technique
🔌 La règle de dépendance
Tout dépend du domaine, le domaine ne dépend de rien
đź’ˇ Aujourd'hui, on entre dans le domaine
On découvre les 3 briques qui composent le cœur métier : Entity, Value Object, Aggregate
Module 2
L'objet qui a une identité
Une Entity se reconnaît par son ID, pas par ses attributs
Deux Users avec le même ID sont le MÊME User, même si leurs attributs diffèrent
User #42
Alice
alice@mail.com
User #42
Alice Dupont
alice.d@mail.com
💡 Le nom a changé, l'email a changé — mais c'est TOUJOURS Alice
L'identité (ID) persiste à travers les mutations. C'est ça, une Entity.
Une Entity a un ID, et ses attributs peuvent muter
// domain/entities/User.ts
export class User {
constructor(
private readonly id: string,
private name: string,
private email: string
) {}
// L'ID ne change jamais — readonly
getId(): string { return this.id; }
// Les attributs peuvent muter
rename(newName: string): void {
this.name = newName;
}
}
đź’ˇ L'ID est readonly, les autres champs sont mutables
On modifie l'Entity en place — elle garde la même identité
Deux Entities sont égales si leur ID est identique — peu importe les attributs
// La méthode equals compare l'ID, pas les attributs
export class User {
// ... constructor, getters, rename ...
equals(other: User): boolean {
return this.id === other.id;
}
}
// Usage
const alice1 = new User("42", "Alice", "a@mail.com");
const alice2 = new User("42", "Alice D.", "ad@mail.com");
alice1.equals(alice2); // true ! MĂŞme ID = mĂŞme User
đź’ˇ Entity : comparaison par ID
Les attributs diffèrent, mais l'identité est la même → c'est le même objet
Module 3
L'objet défini par ses attributs
Un Value Object se reconnaît par ses attributs, pas par un ID
100 EUR = 100 EUR. Deux billets de 100€ valent la même chose, peu importe leur "identité"
100 €
billet #1
100 €
billet #2
💡 Pas d'identité — seulement des valeurs
Si les attributs sont identiques, les deux Value Objects sont égaux. Point.
Un Email se valide à la création et ne change plus jamais
// domain/valueObjects/Email.ts
export class Email {
constructor(private readonly value: string) {
// Validation Ă la construction
if (!value.includes("@")) {
throw new Error("Email invalide");
}
}
// Pas de setter — on ne modifie pas
getValue(): string { return this.value; }
equals(other: Email): boolean {
return this.value === other.value;
}
}
💡 Tout est readonly — pas de mutation possible
La validation dans le constructeur garantit qu'un Email est TOUJOURS valide
Un Value Object peut contenir plusieurs champs — ici montant + devise
// domain/valueObjects/Money.ts
export class Money {
constructor(
private readonly amount: number,
private readonly currency: string
) {
if (amount < 0) throw new Error("Montant négatif");
}
equals(other: Money): boolean {
// Comparaison par TOUS les attributs
return this.amount === other.amount
&& this.currency === other.currency;
}
}
// 100 EUR ≠100 USD — devise différente
new Money(100, "EUR").equals(new Money(100, "USD")) // false
đź’ˇ Un VO peut avoir plusieurs champs !
Money = montant + devise. Les deux attributs participent à l'égalité.
❌ On mute le Value Object
const email = new Email("a@mail.com");
email.value = "b@mail.com"; // ❌
// Impossible : readonly !
Un VO est immutable — on ne peut pas le modifier
✅ On crée une nouvelle instance
const oldEmail = new Email("a@mail.com");
const newEmail = new Email("b@mail.com");
// âś… Nouvel objet, ancien intact
On remplace l'ancien par le nouveau — l'ancien reste valide
💡 L'immutabilité = sécurité
Pas d'effet de bord, pas de mutation surprise. Un VO valide le reste toujours valide.
Critère
Entity 🪪
Value Object đź”’
Identité
Par ID
Par attributs
Mutation
Mutable (change en place)
Immutable (on en crée un nouveau)
Égalité
Même ID = égal
Mêmes attributs = égal
Exemple
User, Order, CompteBancaire
Email, Money, Adresse
Module 4
La frontière de cohérence
Un Aggregate regroupe des objets qui doivent rester cohérents ensemble
L'Aggregate Root est le SEUL point d'entrée — on ne touche jamais aux objets internes directement
Aggregate Root
📦 Order
le gardien
Objets internes
đź§± OrderItems
cachés derrière la racine
💡 Order protège ses OrderItems
On ne peut pas ajouter un OrderItem directement — on passe par Order.addItem()
La racine protège ses objets internes via des méthodes dédiées
// domain/aggregates/Order.ts
export class Order {
private items: OrderItem[] = [];
constructor(private readonly id: string) {}
// SEUL point d'entrée pour modifier les items
addItem(product: string, qty: number): void {
if (qty <= 0) throw new Error("Quantité invalide");
this.items.push(new OrderItem(product, qty));
}
getTotal(): number {
return this.items.reduce((sum, i) => sum + i.getSubtotal(), 0);
}
}
💡 items est private — on ne peut pas y accéder de l'extérieur
La racine valide les règles (quantité > 0) avant d'accepter un changement
❌ Accès direct aux objets internes
const order = repo.find("42");
order.items[0].qty = -5; // ❌
// Contourne les règles métier !
On mutile l'objet interne sans validation — incohérence garantie
âś… On passe par la racine
const order = repo.find("42");
order.updateItemQty(0, 5); // âś…
// La racine valide la règle
La racine applique ses règles — la cohérence est préservée
💡 La racine est le gardien des règles
On ne touche JAMAIS aux objets internes directement — toujours via l'Aggregate Root
Vous avez déjà fait un agrégat sans le savoir !
Le CompteBancaire de la Semaine 8 protège ses Transactions — c'est exactement le pattern Aggregate
📦 CompteBancaire = Aggregate Root
Le gardien : il valide les retraits, calcule le solde
đź§± Transactions = objets internes
Cachés derrière le compte — on ne les manipule pas directement
đź’ˇ DDD donne un nom Ă ce que vous faites dĂ©jĂ
CompteBancaire.protégerTransactions() = Aggregate Root protégeant ses objets internes
❌ Confondre Entity et Aggregate
✅ Un Aggregate EST une Entity (la racine), mais avec des objets internes à protéger
❌ Penser qu'un Value Object ne contient qu'un seul champ
âś… Un VO peut avoir plusieurs champs : Money = montant + devise, Adresse = rue + ville + code postal
❌ Oublier l'immutabilité des Value Objects dans le code
✅ Tout est readonly — on ne modifie pas un VO, on en crée un nouveau
Pour chaque concept, dites : Entity, Value Object, ou Aggregate ?
Justifiez votre choix en une phrase
👤
Un Client
A-t-il une identité qui persiste ?
📍
Une Adresse
Deux adresses identiques sont-elles la mĂŞme ?
📦
Un Panier avec ses articles
Qui protège la cohérence des articles ?
đź“…
Une Date
A-t-elle besoin d'une identité ?
đź’ˇ Indices
Client = Entity · Adresse = VO · Panier = Aggregate · Date = VO
🪪
Entity = identité
Comparaison par ID, mutable
đź”’
Value Object = attributs
Comparaison par valeur, immutable
📦
Aggregate = frontière de cohérence
La racine protège ses objets internes
🏦
CompteBancaire = déjà un agrégat
Vous le faisiez depuis S8 !
Classez vos propres objets : Entity, VO, ou Aggregate ?
Demain : Domain Events — comment les objets communiquent