Entité, Value Object, Agrégat

Les trois briques du Domain Driven Design

Bootcode IWA-S04 — Semaine 17, Jour 2

Objectifs de la leçon

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

Plan du cours

1

Rappel rapide du J1

Le vocabulaire DDD qu'on a découvert

2

Entity = identité

User change de nom mais reste le mĂŞme User

3

Value Object = attributs

100 EUR = 100 EUR, pas d'identité

4

Aggregate = frontière de cohérence

Order protège ses OrderItems

5

Exercice live

Classifier des concepts réels en Entity / VO / Aggregate

Module 1

Rappel du J1

Le vocabulaire DDD qu'on a découvert hier

Hier, on a posé les bases

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'Entity

L'objet qui a une identité

Entity = 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.

🪪 Entity : User avec ID

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é

Comparaison par ID

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

Le Value Object

L'objet défini par ses attributs

Value Object = 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.

đź”’ Value Object : Email immutable

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

đź’° Value Object multi-champs : Money

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é.

Immutabilité : on ne modifie pas, on crée un nouveau

❌ 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.

Entity vs Value Object

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

L'Agrégat

La frontière de cohérence

Aggregate = 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

protège

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()

📦 Aggregate Root : Order avec OrderItems

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

L'Aggregate Root est le gardien

❌ 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

Rappel : CompteBancaire de S8

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

Pièges courants à éviter

❌ 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

Exercice live : classifier des concepts

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

Ă€ retenir !

🪪

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 !

Questions ?

Classez vos propres objets : Entity, VO, ou Aggregate ?

Demain : Domain Events — comment les objets communiquent