02 — Value Objects vs Entités : quelle différence concrète
Ce que tu vas apprendre
- La distinction fondamentale entre identité et valeur
- Pourquoi un
Userest une Entité et unEmailest un Value Object - Comment les Entités et les Value Objects se composent
- Un tableau de décision pour ne plus hésiter
Prérequis
La question centrale : "lequel" ou "quoi" ?
La différence entre une Entité et un Value Object tient à une seule question :
Est-ce que l'identité de cet objet compte, ou uniquement sa valeur ?
- Deux billets de 20€ sont interchangeables. Peu importe lequel tu utilises : la valeur est identique. C'est un Value Object.
- Deux utilisateurs avec le même prénom ne sont pas interchangeables. Ce sont deux personnes distinctes avec des historiques différents. C'est une Entité.
Value Object : "quoi" — Entité : "lequel"
Un billet de 20€ vaut 20€ peu importe sa série. Un compte bancaire N°123456 est distinct du compte N°789012 même s'ils ont le même solde.
Les deux définitions côte à côte
Entité
Une Entité :
- a un identifiant unique (ID) qui la distingue des autres
- peut changer de valeur au fil du temps (l'email d'un utilisateur peut être modifié)
- deux entités avec les mêmes données mais des IDs différents sont deux objets distincts
typescriptclass User {
constructor(
readonly id: UserId, // Identité — ne change jamais
private email: Email, // Valeur — peut changer
private name: string
) {}
changeEmail(newEmail: Email): void {
this.email = newEmail; // L'entité mute — c'est normal
}
equals(other: User): boolean {
return this.id.equals(other.id); // Égalité par ID, pas par valeur
}
}
const alice1 = new User(UserId.create("uuid-1"), Email.create("alice@exemple.fr"), "Alice");
const alice2 = new User(UserId.create("uuid-2"), Email.create("alice@exemple.fr"), "Alice");
alice1.equals(alice2); // false — IDs différents, même si toutes les autres données sont identiques
Value Object
Un Value Object :
- n'a pas d'identifiant — il n'a pas besoin d'en avoir
- est immuable — jamais de setter
- deux Value Objects avec les mêmes données sont identiques et interchangeables
typescriptconst email1 = Email.create("alice@exemple.fr");
const email2 = Email.create("alice@exemple.fr");
email1.equals(email2); // true — même valeur → même objet pour le domaine
Exemples concrets : Entité ou Value Object ?
Utilisateur (`User`)
L'utilisateur Alice est Alice même si elle change d'email, de mot de passe ou de nom. Son identité ne change pas. C'est une Entité.
typescriptclass User {
constructor(
readonly id: UserId,
private email: Email,
private name: string,
private readonly createdAt: Date
) {}
changeEmail(newEmail: Email): void {
this.email = newEmail;
}
getEmail(): Email {
return this.email;
}
equals(other: User): boolean {
return this.id.equals(other.id);
}
}
Email (`Email`)
Un email alice@exemple.fr est identique à un autre alice@exemple.fr. Il n'a pas d'histoire, pas d'identité propre. C'est un Value Object.
typescriptclass Email {
private constructor(private readonly value: string) {}
static create(raw: string): Email {
const trimmed = raw.trim().toLowerCase();
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(trimmed)) {
throw new Error(`"${raw}" n'est pas un email valide`);
}
return new Email(trimmed);
}
equals(other: Email): boolean {
return this.value === other.value;
}
toString(): string {
return this.value;
}
}
Commande (`Order`)
Une commande a un numéro de commande unique, un historique de statuts, des articles qui peuvent être ajoutés ou retirés. C'est une Entité.
Prix (`Money`)
100 EUR est toujours 100 EUR. Deux instances de Money(100, "EUR") sont interchangeables. C'est un Value Object.
Comment les Entités utilisent les Value Objects
Les Entités sont composées de Value Objects. Ce n'est pas l'un ou l'autre — c'est les deux ensemble.
typescript// Value Objects
class UserId {
private constructor(private readonly value: string) {}
static create(value: string): UserId {
if (!value.match(/^[0-9a-f-]{36}$/)) {
throw new Error("UUID invalide");
}
return new UserId(value);
}
equals(other: UserId): boolean {
return this.value === other.value;
}
toString(): string {
return this.value;
}
}
class Email {
private constructor(private readonly value: string) {}
static create(raw: string): Email {
const trimmed = raw.trim().toLowerCase();
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(trimmed)) {
throw new Error(`Email invalide : ${raw}`);
}
return new Email(trimmed);
}
equals(other: Email): boolean {
return this.value === other.value;
}
toString(): string {
return this.value;
}
}
// Entité — composée de Value Objects
class User {
constructor(
readonly id: UserId, // VO
private email: Email, // VO
private name: string
) {}
changeEmail(newEmail: Email): void {
this.email = newEmail;
}
getEmail(): Email {
return this.email;
}
equals(other: User): boolean {
return this.id.equals(other.id);
}
}
// Usage
const user = new User(
UserId.create("550e8400-e29b-41d4-a716-446655440000"),
Email.create("alice@exemple.fr"),
"Alice"
);
user.changeEmail(Email.create("alice.new@exemple.fr"));
Ce que ça apporte
UserId garantit que l'ID est un UUID valide. Email garantit que l'email est bien formé et normalisé.
User (l'Entité) peut changer d'email au fil du temps — changeEmail remplace le VO. L'identité de l'utilisateur (id) ne change jamais.
Résultat : le code de domaine est expressif. user.changeEmail(Email.create(...)) ne peut recevoir qu'un email valide — le compilateur l'interdit autrement.
En Python :
pythonfrom dataclasses import dataclass
import re
import uuid
@dataclass(frozen=True)
class UserId:
value: str
def __post_init__(self) -> None:
try:
uuid.UUID(self.value)
except ValueError:
raise ValueError(f"UUID invalide : {self.value}")
@dataclass(frozen=True)
class Email:
value: str
def __post_init__(self) -> None:
normalized = self.value.strip().lower()
if not re.match(r'^[^\s@]+@[^\s@]+\.[^\s@]+{{body}}#x27;, normalized):
raise ValueError(f"Email invalide : {self.value}")
object.__setattr__(self, 'value', normalized)
class User:
"""Entité — a une identité, peut muter."""
def __init__(self, user_id: UserId, email: Email, name: str) -> None:
self._id = user_id
self._email = email
self._name = name
@property
def id(self) -> UserId:
return self._id
@property
def email(self) -> Email:
return self._email
def change_email(self, new_email: Email) -> None:
self._email = new_email # L'entité mute — c'est attendu
def __eq__(self, other: object) -> bool:
if not isinstance(other, User):
return NotImplemented
return self._id == other._id # Égalité par ID
def __hash__(self) -> int:
return hash(self._id)
Tableau de décision
| Question | Réponse → | Type |
|---|---|---|
| Deux instances avec les mêmes données sont-elles interchangeables ? | Oui | Value Object |
| L'objet a-t-il un cycle de vie (créé, modifié, supprimé) ? | Oui | Entité |
| L'objet doit-il être retrouvé par un identifiant ? | Oui | Entité |
| L'objet représente-t-il une mesure, un montant, une adresse ? | Oui | Value Object |
| Est-ce que "lequel" compte plus que "quoi" ? | Oui → lequel | Entité |
Cas limites : l'adresse
Une adresse est-elle une Entité ou un Value Object ?
Ça dépend du contexte :
- Dans un carnet d'adresses (tu veux retrouver "l'adresse de livraison principale") → Entité avec un ID
- Dans une commande (l'adresse est une photo de l'adresse au moment de la commande, elle ne doit pas changer si l'utilisateur met à jour la sienne) → Value Object
Le même concept peut être modélisé différemment selon le cas d'utilisation. Il n'y a pas de réponse universelle — seulement le contexte métier.
Résumé
| Value Object | Entité | |
|---|---|---|
| Identifiant | Non | Oui — ID unique |
| Mutabilité | Immuable | Mutable |
| Égalité | Par valeur | Par identifiant |
| Exemple | Money, Email, DateRange |
User, Order, Product |
Les Entités se composent de Value Objects. Un User (Entité) a un UserId (VO) et un Email (VO). Cette composition rend le code de domaine expressif et les états invalides impossibles.
Étape suivante : 03 — Tester les Value Objects — stratégies de test, cas limites et patterns Vitest/pytest.