Entités et objets valeur
VoletTactique Les deux briques de base du modèle — et le choix entre les deux est plus déterminant qu’il n’y paraît.
Warning
Rappel de dosage avant d’entrer dans le détail : les patrons tactiques ne sont rentables que dans le cœur de métier. Sur un sous-domaine de support, un objet valeur par champ est une dépense sans retour. Voir Sous-domaines.
Le critère : identité ou valeur
Une seule question sépare les deux notions.
Si deux de ces choses ont exactement les mêmes attributs, sont-elles la même chose ?
Si oui, c’est un objet valeur. Deux montants de 12,50 € sont le même montant ; il n’y a pas lieu de les distinguer.
Si non, c’est une entité. Deux personnes qui portent le même nom, la même date de naissance et la même adresse restent deux personnes.
La réponse dépend du contexte, pas de la nature des choses. Un billet de banque est un objet valeur pour un commerçant — seule sa valeur compte — et une entité pour la banque centrale, qui suit son numéro de série. Le même objet du monde réel, deux modélisations, parce que deux contextes.
Les objets valeur
Ce sont eux qui font l’essentiel du travail dans un bon modèle, et ce sont eux qu’on oublie. Trois propriétés les définissent.
Ils sont définis par leurs attributs, donc comparés par valeur et non par référence.
Ils sont immuables. On ne modifie pas un montant, on en calcule un autre. Cela supprime d’un coup toute une famille de bugs — l’objet partagé modifié par inadvertance — et permet de les échanger librement.
Ils valident à la construction. Un objet valeur invalide n’existe pas. Cela
retire la validation de partout ailleurs : si vous détenez un Email, il est bien
formé, et il n’y a plus lieu de le vérifier.
Note
Les trois langages n’ont pas le même effort à fournir. Python obtient
l’immuabilité et l’égalité par valeur d’un seul frozen=True. Rust les obtient par
défaut — Copy et PartialEq dérivés, champs privés, aucun constructeur
implicite — au point que l’objet valeur y est le style naturel plutôt qu’une
discipline. TypeScript demande le plus de travail : constructeur privé, fabrique
statique, et une méthode egale écrite à la main puisque === compare les
références.
L’obsession des primitifs
C’est la première cause de modèles pauvres, et l’objet valeur en est le remède.
Un domaine décrit avec des string, des number et des boolean ne porte aucune
règle. Comparez deux signatures :
La première accepte reserver(spectacleId, clientId, prix, places) — les
paramètres inversés, deux fois — et compile parfaitement. La seconde ne compile
pas. Aucun test n’a été écrit, et une classe entière de bugs vient de disparaître.
Le gain va plus loin que la sécurité de type. Les règles trouvent enfin un
endroit : où mettre « un montant ne s’additionne qu’à un montant de même devise »,
si le montant est un number ? Nulle part — donc dans un service, dispersée,
répétée, oubliée quelque part. Sur un objet Montant, il n’y a qu’une réponse
possible.
Les candidats habituels, dans l’ordre où ils se rencontrent : identifiants, montants et devises, quantités et unités, périodes et durées, adresses, courriels et téléphones, codes et références métier, pourcentages et taux, plages de valeurs.
Tip
Un signe qui ne trompe pas : une méthode qui prend trois string à la suite. Non
seulement l’appelant peut les intervertir, mais le fait qu’il puisse signifie
que le modèle ne sait pas les distinguer.
Les entités
Une entité a une identité qui persiste à travers ses changements d’état. La commande n° 4711 reste la même commande après ajout d’une ligne, changement d’adresse ou passage en expédition.
Trois conséquences pratiques :
L’égalité se fait sur l’identifiant, jamais sur les attributs. Deux instances chargées séparément avec le même identifiant sont la même entité.
L’identifiant est généré par le domaine, pas par la base de données. C’est un détail qui a de grandes conséquences : un identifiant attribué à l’insertion oblige à sauvegarder pour connaître l’identité, ce qui interdit de construire un graphe d’objets cohérent en mémoire, casse les tests sans base, et complique la publication d’événements. Un UUID — ou tout autre identifiant généré côté domaine — supprime le problème.
L’état mute, mais par des méthodes qui portent un nom métier. Le point le plus
important, et la frontière exacte entre un modèle riche et un
modèle anémique : pas de setEtat(), mais confirmer(),
suspendre(motif), annuler(raison).
Ce qui n’est pas une entité
Deux confusions valent d’être nommées, parce qu’elles sont systématiques.
Une entité n’est pas une table. Le modèle relationnel et le modèle de domaine répondent à des contraintes différentes : normalisation d’un côté, expression des règles de l’autre. Une entité peut correspondre à trois tables, et trois entités à une seule. Laisser le schéma dicter le modèle est le plus court chemin vers un domaine qui parle SQL.
Une entité n’est pas une classe annotée par l’ORM. Le mot « entité » de Hibernate, JPA, TypeORM ou SQLAlchemy désigne un objet mappé à une table, ce qui n’a de commun avec l’entité de DDD que le nom. Faire porter les deux rôles au même objet est possible et fréquent, mais ce n’est pas neutre : les contraintes de l’ORM — constructeur sans argument, champs mutables, accesseurs publics, chargement paresseux — sont exactement l’inverse de ce qu’un modèle riche exige.
Tip
Le compromis pragmatique consiste à garder deux représentations et à traduire dans le dépôt : le modèle de domaine d’un côté, le modèle de persistance de l’autre. C’est du code de correspondance supplémentaire — la critique la plus fondée contre cette approche, reconnue comme telle en architecture hexagonale — et c’est ce qui permet aux deux d’évoluer indépendamment.
Où mettre le comportement
La règle est simple à énoncer et exigeante à tenir : le comportement va là où sont les données dont il a besoin.
Une règle qui ne dépend que d’un montant va sur Montant. Une règle qui ne dépend
que de l’état d’une réservation va sur Reservation. Une règle qui a besoin de
deux entités distinctes ne va sur ni l’une ni l’autre : c’est le cas du
service de domaine.
Ce qui reste est le point d’entrée de la notion suivante. Une réservation ne se valide pas seule : il faut vérifier que les places sont disponibles dans la séance, ce qui pose la question de savoir jusqu’où va la cohérence — et donc celle des agrégats.
Références
- Eric Evans, Domain-Driven Design, chapitres 5 et 6
- https://martinfowler.com/bliki/ValueObject.html
- https://refactoring.guru/fr/smells/primitive-obsession