Il y a 5 ans -
Temps de lecture 22 minutes
Craft – Les patterns tactiques du DDD
Le DDD (ou Domain-Driven Design) est une approche de la conception logicielle qui préconise, entre autres, de mettre le domaine métier au centre du développement logiciel. Cette approche est globale car elle propose des outils de conception à la fois au niveau du code, au niveau de l’organisation d’un projet et même au niveau stratégique de toute une organisation. Historiquement, c’est le livre Domain-Driven Design : Tackling Complexity in the Heart of Software écrit par Eric Evans en 2003 qui pose les bases du DDD.
Ce livre insiste ainsi sur le fait que le code doit refléter le modèle du métier et que les problématiques techniques ne doivent pas interférer dans les développements dédiés au domaine métier. Mais atteindre ce but n’est pas chose facile. En effet, il arrive souvent qu’un code métier doive être affecté par des problématiques transverses liées à un aspect technique (persistance…). Conscient de cette difficulté, Eric Evans définit dans son livre un certain nombre de patterns qui nous aident à exprimer les problématiques métiers dans le code : les patterns tactiques.
Dans l’optique de se familiariser avec les patterns tactiques, nous avons organisé un atelier au sein de Xebia, durant notre XKE, afin d’explorer et d’assimiler les différents patterns proposés par Eric Evans. Le format de cet atelier a été inspiré par celui utilisé lors du Meetup DDD Paris de Novembre 2017. Ainsi, après une introduction d’Édouard Siha et de Clément Heliou, l’atelier consistait à se séparer en petits groupes dont chacun devait étudier un pattern précis. Puis, nous avons partagé notre compréhension et nos avis sur ces différents patterns.
Collaborative work on #DDD tactical patterns during our XKE. Based on format experienced w/ @tpierrain @jgrodziski pic.twitter.com/DG7gas1gVX
— Clément HELIOU (@c_heliou) 12 mars 2018
Cet atelier est le premier d’une série de trois ateliers dont le but est de faire un tour d’horizon des principaux concepts du DDD ; les deux autres ateliers ayant pour sujets respectifs le « Supple Design » et les « patterns stratégiques ».
Le but de cet article est donc de vous présenter les fruits de ce travail collectif : nous vous présenterons ainsi chacun des patterns tactiques énoncés par Eric Evans, avec leurs forces, leurs faiblesses, et nous les illustrerons par un ou plusieurs exemples . Nous préciserons aussi les cas où ces patterns ont évolué depuis l’année où ils ont été définis.
Entities
Les Entities (ou entités) sont des objets métiers dont l’identité arbitraire reste la même au cours du temps, indépendamment de leurs attributs qui peuvent évoluer. Par exemple, si on doit modéliser un être humain, celui-ci peut voir ses attributs évoluer (âge, couleur de cheveux…). Pourtant, son identité reste la même.
Du coup, pour modéliser une Entity au niveau du code, on utilise des objets classiques dont l’un des attributs sera constant et constituera son identifiant. Ainsi, pour savoir si deux objets représentent la même entité, il suffira de comparer leurs identifiants. Dans l’exemple précédent d’une personne, on pourra choisir comme identifiant le numéro de sécurité sociale, car ce dernier a l’avantage d’être constant et unique.
Modéliser un objet en tant qu’Entity participe à traduire au mieux les concepts du métier. Mais cela permet aussi d’attirer l’attention sur les implications de cette modélisation. En effet, les Entities ont un cycle de vie précis, doivent souvent être persistées, sont plus complexes à maintenir et ne sont pas thread-safe (il faut éviter que deux composants d’un programme modifient le même objet en même temps, car cela risque de mener à des incohérences).
Enfin, remarquons que lorsqu’on implémente une Entity, il n’est pas toujours évident de trouver un identifiant « naturel ». Par exemple, si, comme on l’a vu plus haut, le numéro de sécurité sociale est un bon candidat pour être un identifiant d’une personne (et encore, uniquement en France), il n’est peut être pas l’identifiant le plus intuitif : on aurait plutôt pensé aux noms et prénoms, mais ceux-ci sont rarement uniques. Enfin, si c’est notre application qui doit générer les identifiants, il faut faire attention à bien garantir l’unicité de ces identifiants car ce n’est pas toujours trivial : si on utilise un identifiant auto-incrémenté, il faut vérifier la cohérence de l’incrément dans les environnements distribués ; si on utilise des timestamps, il faut gérer les cas des requêtes quasi simultanées…
On comprend donc pourquoi la gestion des Entities est complexe et pourquoi on préfère modéliser un objet métier par le pattern Value Object lorsqu’une Entity n’est pas absolument nécessaire.
Exemple
Afin de rendre plus concret ce pattern, prenons un exemple dans le domaine de l’automobile. Supposons que nous devons écrire des applications gérant une société de location de véhicules. Une automobile est un objet qui évolue : elle peut être louée, des pièces peuvent être changées, etc. On comprend donc bien que cet objet sera modélisé dans l’application en tant qu’Entity.
Comme souvent avec les Entities, il faut trouver un identifiant pour chaque véhicule. On pourrait penser à la plaque d’immatriculation, mais les règles de ces plaques diffèrent d’un pays à l’autre. Heureusement, il existe un Vehicle Identification Number qui fait office d’identifiant naturel, sans quoi, il aurait fallu trouver un identifiant technique avec tous les problèmes de vérification d’unicité que cela pose.
Value Objects
Les Value Objects sont des objets métiers définis par leurs attributs : ils n’ont pas d’identité. Par exemple, si on change un attribut (mois, année…) d’une date, on change la date : il ne s’agit plus de la même date. Du coup, pour modéliser un tel objet, on utilisera des objets immuables : deux objets seront égaux si et seulement si tous leurs attributs sont égaux. Ces objets n’ont donc pas d’état interne qui évolue au cours du temps.
« C’est ce qu’ils sont qui est important, pas qui ils sont » – J. Grodziski
Le fait que ces objets soient immuables les rend très pratiques à utiliser : ils sont thread-safe, facilement transférables, etc. Les Value Objects ne sont pas de simples DTO ; on peut (et l’on doit) mettre de la logique métier dans ces objets. C’est même un excellent moyen d’absorber la complexité d’un métier.
Dans un projet, distinguer les objets qui sont des Entities de ceux qui sont des Value Objects est un des meilleurs moyens pour simplifier le code et augmenter la compréhension du domaine métier.
Remarques :
- Les Value Objects sont un excellent point de départ pour démarrer le DDD dans un code existant. Repérer quels sont les Value Objects et encapsuler les règles métier en leur sein permet non seulement de mieux exprimer le domaine mais aussi d’« absorber » la complexité du code.
- La distinction Entities/Value Objects est relative à un contexte : en effet, un même concept peut être représenté par un Value Object ou par une Entity selon le domaine dans lequel on se trouve. L’exemple classique est le billet de banque : pour un commerçant deux billets de banque de même valeur sont tout à fait interchangeables (Value Object) alors que pour la Banque de France qui imprime et détruit ces billets, un billet de banque précis a un identifiant et un cycle de vie (Entity).
- Depuis peu, on commence à parler de Value Type plutôt que de Value Object car ce dernier terme est un oxymore (un objet est censé avoir un état interne qui évolue alors qu’une valeur est censée être quelque chose de constant).
Exemple
Restons dans le domaine automobile de l’exemple précédent, et supposons que l’on a besoin de savoir où se trouve une voiture. La position d’une voiture est un bon exemple de Value Object : une position n’a pas d’identification et est totalement définie par sa valeur (exprimée en coordonnées GPS). Cela dit, cet objet n’a pas à être un simple DTO, il peut contenir un certain nombre de méthodes qui ont un sens métier : la distance à vol d’oiseau entre deux positions, le chemin routier le plus court entre deux positions, le pays où se trouve la position…
Aggregates
Dans un système complexe (par exemple : distribué, multi-threadé…), il arrive souvent qu’il faille assurer la cohérence d’un ensemble d’objets. Or, cette cohérence peut vite se révéler être un cauchemar à maintenir dans le code.
Le pattern Aggregate apporte une solution à ce problème. Un Aggregate est un ensemble d’objets métiers (Value Objects ou Entities) liés ensemble. Parmi ces objets, un seul (en général, une Entity) aura un rôle particulier : le rôle de racine de l’Aggregate. Tous les changements apportés à l’Aggregate par le reste du code devront passer par une méthode de la racine. Il est donc interdit de modifier l’Aggregate en modifiant directement un objet non-racine.
Du coup, la racine aura, seule, la charge de s’assurer de la cohérence de l’Aggregate. Un Aggregate permet donc d’établir un périmètre de cohérence autour d’un ensemble d’objets. Ainsi, les Aggregates sont un bon moyen pour implémenter les invariants métiers et les règles de gestion. Afin d’assurer la cohérence d’un Aggregate au sein d’un environnement multi-threadé, tous les appels de méthodes à l’intérieur d’un Aggregate devront être faits de manière synchrone.
Une bonne analogie pour comprendre le principe du pattern est celle d’un zoo : il faut éviter que plusieurs personnes puissent allouer les cages aux différents animaux car, en cas mauvaise synchronisation, les lions et les antilopes risquent de se trouver dans la même cage. Il vaut mieux demander à l’objet racine (ici : le gardien de zoo) de s’occuper de la cohérence du placement des animaux en cage.
De plus, la racine d’un Aggregate, étant la plupart du temps une Entity, un Aggregate est soumis aux mêmes contraintes que les Entities : ils doivent respecter un cycle de vie et ne sont pas thread-safes. Par contre, si un programme doit traiter plusieurs Aggregates , il n’est pas interdit de partager ce traitement entre plusieurs noeuds/threads, à la condition que chaque Aggregate ne soit modifié que par un seul noeud. Une bonne analogie de ce partage dans le monde réel est l’exemple d’une usine de voitures contenant plusieurs chaines de montage : les futurs véhicules sont répartis au sein des différentes chaines de montage selon leur identifiant, chaque véhicule ne pouvant être traité que par une seule chaine de montage à la fois.
Remarque : étant donné qu’un Aggregate doit assurer la cohérence du modèle, il est parfois tentant d’inclure tout le modèle dans un Aggregate. Mais il est aussi important de faire en sorte que les Aggregates soient les plus petits possibles. En effet, plus un Aggregate est gros, plus sa maintenance sera difficile et plus la surface dans laquelle les appels ne peuvent être que synchrones augmente.
Exemple
Dans l’exemple du domaine métier lié à l’automobile, si l’on veut modéliser en détail une voiture, il faut prendre en compte le fait qu’une voiture contient des pièces qui communiquent entre elles. Ainsi une voiture contient un moteur, des roues, des freins. Toutes ces parties communiquent entre elles : le moteur fait accélérer les roues, tandis que les freins les bloquent, et le volant les fait tourner. Afin de garantir la cohérence des données (éviter qu’au niveau informatique, la voiture tourne à droite, alors que les roues sont orientées à gauche), on peut modéliser la voiture comme un Aggregate de pièces, et interdire à tout objet externe d’appeler les méthodes des objets de cet Aggregate excepté la racine de cet Aggregate : ici la voiture elle-même.
Factories
Parfois, la création d’un Aggregate (ou même d’un Value Object particulièrement grand) peut se révéler très compliquée, surtout si l’objet en lui-même est déjà complexe. De plus, pour créer un Aggregate, il faut parfois créer ses composants, ce qui risque de les exposer, alors que le principe des Aggregates est justement d’éviter les manipulations externes des composants internes.
Dans ces cas-là, le pattern Factory apporte une solution en transférant la responsabilité de la création de l’objet à un objet dédié : la Factory. Celle-ci fait partie du modèle du domaine même si elle n’a pas vraiment de responsabilité métier dans le domaine. Ce pattern est donc très proche du design pattern classique du GoF de la programmation orientée objet.
Par contre, ce pattern ne doit pas être systématiquement utilisé à chaque création d’objet. Sinon, il créerait inutilement de la complexité supplémentaire.
Remarquons que depuis la date de l’écriture du Blue Book, le pattern Factory se fait de plus en plus discret, remplacé le plus souvent par le pattern Builder. Ce dernier est en grande partie équivalent mais a l’avantage d’être plus flexible et d’avoir une interface souvent plus claire.
Exemple
Supposons que l’on veuille écrire une application gérant le parc d’une entreprise de location de voitures, on modélisera donc des objets Voiture. Or, si l’on modélise un tel objet comme un Aggregate (comme vu au chapitre précédent), la création d’une nouvelle voiture avec un moteur particulier, des freins d’origine et une carrosserie refaite sera non seulement complexe, mais nécessitera aussi que le code métier manipule des objets internes aux Aggregates. Pour éviter cela, on peut donner à un objet particulier la responsabilité de créer cet objet, afin d’isoler la création de l’objet du reste du code métier.
Services
Il est des cas où les Entities, Value Objects, et Aggregates ne suffisent pas pour contenir toute la logique d’un domaine métier.
Dans ce cas on pourra utiliser des Services qui sont des classes qui effectuent les traitements métiers qui ne peuvent être réalisés de manière satisfaisante par tout autre objet métier. C’est typiquement le cas des transformations d’objets métiers en d’autres objets.
Un autre exemple est une transaction monétaire entre deux comptes bancaires : quel objet va s’occuper de l’orchestration de la transaction au niveau du code ? Implémenter la transaction dans l’un des deux objets « compte bancaire » serait maladroit car au niveau métier, cela ne fait guère de sens. Du coup, implémenter la transaction au sein d’une classe de type Services est une meilleure solution.
Attention toutefois à implémenter uniquement des règles métier qui ne peuvent être prises en compte par aucun objet métier (Entities, Value Object ou Aggregates). Sans quoi on risquerait d’aboutir à un Anemic Domain Model, c’est-à-dire une modélisation du métier très pauvre : les Value Objects, Aggregates et Entities risqueront d’être de simples DTO et toute la logique métier serait dans des classes Services obèses et peu maintenables.
Exemple
Toujours dans le cas d’une application de gestion d’une entreprise de location de voiture, supposons que nous voulions créer une méthode calculant le prix de location d’une voiture. Pour cela, il faudra coder une fonction computePrice
qui calculera le prix de location à partir du véhicule, de la date de location, du lieu de la location, voire du client lui-même (car il peut bénéficier de certaines réductions). S’il est clair que cette méthode devra faire partie de la couche domaine, dans quel type d’objet doit-on placer cette méthode ? Doit-on la coder dans l’Aggregate Vehicule
, dans la Value Object Date
, ou dans l’Entity Client
? Il est clair qu’aucun de ces objets ne convient parfaitement pour abriter la méthode computePrice
. Dans ce genre de cas, la meilleure solution est donc de stocker cette méthode dans une classe Service spécialisée (par exemple RentService
) qui fera partie intégrante du modèle.
Domain Events
Si les Entities, Value Objects et Aggregates permettent de représenter le modèle à un instant t
, il est souvent utile de savoir comment le système en est arrivé là.
Un pattern très utile permet de traiter ce problème : les Domain Events. Ce sont des objets qui modélisent une transaction métier sur une Entity ou un Aggregate. Ces objets ont souvent vocation à être persistés, ce qui permet d’avoir un historique complet des changements qu’a subi le modèle. Un autre avantage des Domain Events est qu’ils permettent de simplifier la synchronisation inter-systèmes. Dès qu’un système a été modifié, il peut émettre un Domain Event pour notifier aux autres systèmes la modification de son état.
Au niveau de l’implémentation, les Domain Events sont des objets immuables, qui contiennent à minima un timestamp pour avoir la date de l’évènement et un numéro de séquence qui identifie le Domain Event dans la séquence qui le contient. Le timestamp seul ne suffit pas toujours à identifier un Domain Event, car il peut y avoir plusieurs évènements durant la même milliseconde, surtout dans les systèmes distribués.
Dans le domaine bancaire, des exemples classiques de Domain Events sont les évènements « Comptes crédités » et « Comptes débités » des Entities « Comptes bancaires ».
Les Domain Events sont un pattern qui devient de plus en plus utilisé actuellement car il est à la base des concepts d’Event Sourcing, qui lui-même se marie très bien avec le pattern d’architecture CQRS.
Remarque : ce pattern n’a en fait pas été défini dans le livre original d’Eric Evans, mais dans la version condensée et complétée que celui-ci a publié en 2015 : DDD Reference.
Exemple
Dans l’exemple d’un parc automobile, une voiture peut être louée, rendue, envoyée en réparation. Ainsi, à chaque fois, que la voiture change d’état (en passant de « louée » à « rendue » par exemple), on peut modéliser ce changement d’état par un objet de type Domain Event. Pour peu que cet objet soit persisté, on pourra ainsi non seulement consulter l’état actuel d’une voiture, mais aussi retracer tout l’historique d’une voiture.
Layered Architecture
Tous les patterns vus jusqu’à présent permettent de modéliser un domaine métier. Mais si l’on n’isole pas le code métier du reste du code, ces patterns perdent beaucoup de leur valeur.
Du coup, une des manières d’isoler le code métier est d’utiliser la classique architecture en couches. Il suffit alors de réserver une de ces couches au code métier afin de séparer les problématiques métiers des autres composants d’un logiciel (interface graphique, couche de données, …).
Toutefois, si l’on reste sur une architecture en couches « classique », la couche « métier » sera dépendante du code des couches du dessous (stockage de données, infrastructure). Le code métier sera ainsi dépendant des choix techniques du projet, et devra donc être modifié si ces choix changent. Or, cela est contradictoire avec un des objectifs du DDD qui est que le code métier ne change que lorsque le métier change (ou lorsque la compréhension du métier change). Pour éviter cet écueil, une astuce consiste à utiliser la technique d’injection de dépendances afin que cela soit la couche technique qui dépende du code métier et non l’inverse. Ainsi, la couche métier ne dépendra d’aucune autre couche, ce sont les autres couches qui dépendront d’elle.
Cette architecture a été énoncée par Eric Evans dans son livre de 2003. Depuis cette époque, cette architecture a été simplifiée et raffinée, en découlant l’Architecture Hexagonale dans laquelle ne subsiste que deux couches : le domaine et l’infrastructure. On peut citer aussi un autre raffinement de la Layered Architecture : l’Onion Architecture qui promeut, au contraire, un plus grand nombre de couches : le domaine lui-même étant divisé en plusieurs sous-couches.
|
|
Architecture en couche classique | Architecture en couche conseillée pour le DDD |
Exemple
Dans notre exemple de parc automobile, supposons que l’on doive prévenir un client que sa voiture est prête. On peut alors proposer une interface ContactAvecLeClient
contenant une méthode prevenirClient
. Si cette interface est définie dans la couche Domaine, son implémentation se fera dans la couche Infrastructure et consistera à envoyer un mail au client. Si demain, les choix technologiques changent et que l’on veut prévenir les clients par SMS, il suffira de changer l’implémentation de l’interface, et cela peut se faire sans rien toucher au code de la couche Domaine.
Repositories
Si les patterns précédents permettent de modéliser le métier, une question n’a pas encore reçu de réponse : comment faire pour stocker les données représentant les objets métiers ?
Si, pour enregistrer/récupérer des Aggregates, on appelle directement des requêtes SQL dans le code métier, non seulement on brise la séparation du code métier et du code technique mais, en plus, on lie notre code à une technologie particulière comme, dans ce cas, les bases de données relationnelles.
Ainsi, le pattern Repository répond à cette problématique en abstrayant le stockage et la récupération des Aggregates. L’interface d’un Repository doit être indépendante de la couche technique et doit avoir un sens métier. En étant indépendant des détails techniques du stockage, ce pattern permet donc de changer de stockage au cours du projet et permet aussi d’augmenter la testabilité du code métier.
Attention à ne pas confondre le pattern Repository du DDD avec de simples Data Access Object (DAO). En effet, ces derniers ne servent qu’à mapper des objets avec des entrées de la base de données, il n’offrent pas une indépendance vis-à-vis de la technique et leur interfaces n’ont généralement pas de sens métier.
Exemple
Dans notre exemple « fil rouge » d’un parc automobile, on peut imaginer devoir coder une interface VehicleRepository
qui contiendra des fonctions pour récupérer les véhicules selon leurs marques, leurs plaques d’immatriculation, voire leurs cibles commerciales (familles, professionnels, …). Vu que l’implémentation de cette interface se situera au niveau de la couche Infrastructure (cf paragraphe précédent), on pourra, en changeant cette implémentation, changer la façon de stocker les données des véhicules. On pourra ainsi, au début du projet, stocker les données dans des fichiers, puis lorsque le projet évoluera, on pourra les stocker dans une base de données. En principe, ces changements n’impacteront en rien le code du domaine qui n’utilisera que l’interface du Repository.
Modules
Les Modules sont un regroupement logique de modules/classes au sein d’une application. Les Modules sont certainement l’un des concepts les plus connus de la programmation et tous les langages ont des moyens pour les implémenter (packages en Java, namespaces en C#…).
Le Domain-Driven Design recommande aussi l’utilisation des Modules. Mais dans le code métier, les classes doivent être regroupées par affinité fonctionnelle plutôt que par les détails d’implémentation (quitte à prendre le risque d’avoir un peu de répétition dans le code).
Une métaphore pour comprendre ce dernier point est l’exemple des ustensiles et des outils : en général, on range ensemble les fourchettes et les couteaux car ils ont des fonctionnalités proches (il servent de couverts). Mais on ne range pas ensemble les couteaux et les scies malgré le fait qu’ils aient tous les deux des lames de métal aiguisées.
Exemple
Dans notre application de gestion d’un parc automobile d’une entreprise de location de voitures, la division de notre application en module pourrait, par exemple, contenir un module gérant les voitures et un autre gérant les clients. En effet, ces modules font sens dans notre domaine : il font clairement partie de notre modèle métier.
Conclusion
Tous les patterns présentés ici ont pour but de faciliter l’expression du modèle du domaine dans le code. En effet, ils aident:
- à séparer la logique métier et la logique technique (Repository, Layered Architecture),
- à organiser le code (Value Object, Entities, Aggregates, Modules, Service),
- à réfléchir aux compromis à faire entre concurrence et cohérence (Aggregates, Domain Events),
- à articuler clairement les relations entre concurrence et logique métier (Domain Event),
- à forcer l’explicitation des concepts métiers dans le code (Value Object, Entities, Domain Events).
Ainsi, appliquer ces patterns permet d’obtenir un code plus clair, plus organisé, mieux adapté aux systèmes distribués, et dans lequel la logique métier se dégage clairement.
De plus, mettre en place ces patterns au sein d’un projet permet d’initier l’appétence au métier pour les développeurs. Ces patterns sont aussi un excellent point de départ pour appliquer les concepts du DDD au sein d’un projet informatique. En particulier, les Value Objects et les Aggregates sont de puissants « absorbeurs » de complexité, et sont très indiqués lorsque l’on commence un refactoring du code.
Toutefois, s’il s’agit d’un très bon point de départ, le DDD ne se limite pas aux patterns tactiques et dispose d’autres outils et approches qui ne se limitent pas au code.
C’est ce que nous verrons dans la suite de notre série : notre prochain article traitera de Supple Design et le dernier présentera les patterns stratégiques du DDD.
Commentaire
5 réponses pour " Craft – Les patterns tactiques du DDD "
Published by Lidonis Calhau , Il y a 5 ans
Bonjour,
Je n’ai pas bien compris le schéma de l’aggregate avec deux types de roue différents.
Merci.
Published by Javier MERCHAN , Il y a 5 ans
Très claire explication d’un sujet souvent présenté comme un amas de buzzwords. Ca m’a donné envie de m’y mettre ! Vivement la suite.
Published by Yassine BSF , Il y a 5 ans
Merci Excellent article , pourriez vous svp nous dire la différence entre le modele Anemic Domain Model et le DDD ? qu’est ce qui est le plus utilisé ? quel modèle est idélal quand on utilise une application avec le framework spring ?
Published by Alexis M. , Il y a 5 ans
Bonjour,
@Lidonis, si j’ai bien compris le schéma, la « Roue » qui contient les méthodes « freiner » et « lacherFrein » devrait s’appeler « Frein » et non « Roue ».
Alexis
Published by Frederyk , Il y a 4 ans
Si je ne m’abuse il y a une coquille au niveau du chapitre Layered architecture: Pour éviter que la couche métier ne dépende de la couche technique il faut utiliser la technique dite « d’inversion de dépendance » par l’utilisation d’interface et non d’injection de dépendance.