Value Objects — 02 — Value Objects vs Entités : quelle différence concrète

La différence entre Value Object et Entité en DDD : identité vs valeur, mutabilité, exemples concrets TypeScript et Python. Comment choisir entre les deux.

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 User est une Entité et un Email est 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.

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