Value Objects — 00 — C'est quoi un Value Object et pourquoi ça change tout

Définition des Value Objects en DDD : 3 propriétés fondamentales, premiers exemples TypeScript et Python. Pourquoi ce pattern élimine une catégorie entière de bugs.

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 :

  • amount peut être négatif — personne ne l'interdit
  • currency peut être "euro", "EUR" ou "€" selon l'auteur du code
  • email peut être "pas-un-email" — aucune vérification
  • clientId et email sont tous les deux des string — 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)
Email 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.

Réservez un audit gratuit de 30 minutes. Je vous montre concrètement ce qu'on peut automatiser.