00 — C'est quoi un Value Object et pourquoi ça change tout
Ce que tu vas apprendre
- Pourquoi les primitives posent problème dans le code métier
- La définition d'un Value Object et ses 3 propriétés fondamentales
- Un premier exemple concret en TypeScript et Python
- Quels concepts métier méritent d'être des Value Objects
Le problème des primitives dans le code métier
Voici une fonction typique dans un projet réel :
typescriptfunction createInvoice(
clientId: string,
amount: number,
currency: string,
email: string
) { ... }
Ce code compile. Il s'exécute. Et il cache plusieurs bugs :
amountpeut être négatif — personne ne l'interditcurrencypeut être"euro","EUR"ou"€"selon l'auteur du codeemailpeut être"pas-un-email"— aucune vérificationclientIdetemailsont tous les deux desstring— on peut les inverser sans erreur de compilation
Ce phénomène a un nom : Primitive Obsession. Tu utilises des types génériques là où ton métier a des contraintes précises.
Les Value Objects règlent ça à la racine.
Définition : un Value Object en 3 propriétés
Un Value Object est un objet qui :
1. Représente un concept métier précis, pas une donnée brute.
Un Email n'est pas une string. Un Money n'est pas un number. Ils portent des règles que la primitive n'a pas.
2. Est immuable. Une fois créé, il ne change pas. Si tu as besoin d'une valeur modifiée, tu crées un nouveau Value Object.
3. Est égal par valeur, pas par référence. Deux Value Objects avec les mêmes données sont interchangeables, peu importe qu'ils soient deux objets distincts en mémoire.
| Immuable | Égal par valeur | Validé à la création |
|---|---|---|
| Aucun setter | a.equals(b) suffit |
État invalide impossible |
Premier exemple : Email
Sans Value Object :
typescriptconst email: string = "pas-un-email";
sendWelcomeEmail(email); // Bug découvert à l'exécution, pas à la compilation
Avec un Value Object :
typescriptclass Email {
private readonly value: string;
private constructor(value: string) {
this.value = value;
}
static create(raw: string): Email {
const trimmed = raw.trim().toLowerCase();
if (!trimmed) {
throw new Error("L'email ne peut pas être vide");
}
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(trimmed)) {
throw new Error(`"${raw}" n'est pas une adresse email valide`);
}
return new Email(trimmed);
}
equals(other: Email): boolean {
return this.value === other.value;
}
toString(): string {
return this.value;
}
}
// Utilisation
const email = Email.create("Alice@Exemple.fr");
console.log(email.toString()); // "alice@exemple.fr" — normalisé automatiquement
Email.create("pas-un-email"); // Error lancée immédiatement
En Python :
pythonimport re
from dataclasses import dataclass
@dataclass(frozen=True)
class Email:
value: str
def __post_init__(self) -> None:
normalized = self.value.strip().lower()
if not normalized:
raise ValueError("L'email ne peut pas être vide")
if not re.match(r'^[^\s@]+@[^\s@]+\.[^\s@]+{{body}}#x27;, normalized):
raise ValueError(f'"{self.value}" n\'est pas une adresse email valide')
object.__setattr__(self, 'value', normalized)
def __str__(self) -> str:
return self.value
# Utilisation
email = Email("Alice@Exemple.fr")
print(email.value) # "alice@exemple.fr" — normalisé
Email("pas-un-email") # ValueError immédiatement
Un Value Object rend l'état invalide impossible à représenter.
Si l'objet existe, il est valide. Tu n'as plus besoin de vérifier la validité à chaque point d'utilisation dans le code.
L'égalité par valeur en pratique
Les objets en TypeScript et Python sont égaux par référence par défaut :
typescriptconst a = Email.create("alice@exemple.fr");
const b = Email.create("alice@exemple.fr");
a === b // false — deux objets différents en mémoire
a.equals(b) // true — même valeur
pythona = Email("alice@exemple.fr")
b = Email("alice@exemple.fr")
a is b # False — deux objets distincts
a == b # True — grâce à __eq__ généré par @dataclass
Deux Value Objects avec la même valeur sont interchangeables. Leur adresse en mémoire n'a aucune importance. C'est la différence fondamentale avec les Entités, où l'identité compte.
L'immuabilité : pourquoi c'est indispensable
Un Value Object ne doit jamais changer après sa création. Voici la différence :
typescript// Dangereux — mutation partagée
class Money {
amount: number;
currency: string;
add(other: Money): void {
this.amount += other.amount; // Modifie l'objet original
}
}
// Correct — chaque opération retourne un nouveau VO
class Money {
private constructor(
readonly amount: number,
readonly currency: string
) {}
static create(amount: number, currency: string): Money {
if (amount < 0) throw new Error("Montant négatif interdit");
if (!["EUR", "USD", "GBP"].includes(currency)) {
throw new Error(`Devise inconnue : ${currency}`);
}
return new Money(amount, currency);
}
add(other: Money): Money {
if (this.currency !== other.currency) {
throw new Error("Impossible d'additionner deux devises différentes");
}
return Money.create(this.amount + other.amount, this.currency);
}
equals(other: Money): boolean {
return this.amount === other.amount && this.currency === other.currency;
}
}
const prix = Money.create(1000, "EUR");
const taxe = Money.create(200, "EUR");
const total = prix.add(taxe); // Nouveau VO — prix et taxe sont inchangés
Quoi modéliser comme Value Object
Voici les concepts métier courants qui bénéficient du pattern :
| Concept | Type primitif naïf | Value Object |
|---|---|---|
| Prix | number |
Money(amount, currency) |
string |
Email(value) |
|
| Téléphone | string |
PhoneNumber(value, country) |
| Plage de dates | Date, Date |
DateRange(start, end) |
| Coordonnées GPS | number, number |
Coordinates(lat, lng) |
| Pourcentage | number |
Percentage(value) — entre 0 et 100 |
| Identifiant typé | string |
UserId(value) — UUID valide |
| Code postal | string |
PostalCode(value, country) |
La règle : si un concept métier a des règles de validation, un format précis, ou une sémantique que la primitive ne capture pas — c'est un candidat.
Résumé
Un Value Object :
- encapsule un concept métier avec ses règles
- valide ses invariants à la création
- est immuable — toute modification retourne un nouvel objet
- est égal à un autre VO si leurs valeurs sont identiques
Ce pattern élimine une catégorie entière de bugs liés aux primitives non contraintes.
Étape suivante : 01 — Implémenter un Value Object en TypeScript et Python — les patterns de construction, la gestion des erreurs et la sérialisation.