Domain Driven Design

La conception pilotée par le domaine métier

Rejoignez notre communauté et venez assister aux Apér'Ops

Le rendez-vous mensuel des Elosiens

Domain Driven Design (en 5min)

La conception pilotée par le domaine (i.e. Domain-Driven Design ou DDD) est une approche de conception logicielle définie par Eric Evans qui vise à accorder de l’importance au domaine métier.En effet, dans la plupart des logiciels, la logique métier qui est implémentée est ce qui constitue la plus grande valeur ajoutée puisque c’est cette logique qui rend le logiciel fonctionnel.

Pourtant très souvent une grande part des développements se concentrent sur d’autres parties comme l’interface graphique, à la persistance des données ou au partage d’informations avec des systèmes externes.

DDD n’est pas une méthode pour concevoir des logiciels mais juste une approche qui permet d’indiquer comment concevoir un logiciel en prenant davantage en compte le domaine métier.

L’intérêt de DDD est:

  • Permettre à l’équipe de créer un modèle et de le communiquer aux experts métier mais aussi à d’autres acteurs de l’entreprise avec les spécifications fonctionnelles, les entités du modèle de données et la modélisation de processus.

  • Le modèle est modulaire et plus facile à maintenir.

  • Il améliore la testabilité et la généricité des objets du domaine métier.

L’approche DDD vise, dans un premier temps, à isoler un domaine métier. Un domaine métier riche comporte les caractéristiques suivantes:

  • Il approfondit les règles métier spécifiques et il est en accord avec le modèle d’entreprise, avec la stratégie et les processus métier.

  • Il doit être isolé des autres domaines métier et des autres couches de l’architecture de l’application.

  • Le modèle doit être construit avec un couplage faible avec les autres couches de l’application

  • Il doit être une couche abstraite et assez séparée pour être facilement maintenue, testée et versionnée.

  • Le modèle doit être concu avec le moins de dépendances possibles avec une technologie ou un framework. Il devrait être constitué par des objets POCO (Plain Old C# Object). Les POCO sont des objets métier disposant de données, de logique de validation et de logiques métier. Il ne doit pas comporter de logique de persistance.

  • Le domaine métier ne doit pas comporter de détails d’implémentation de la persistance.

Le domaine est connu par les spécialistes du domaine qui sont des experts. Il faut donc rencontrer ces experts pour comprendre toutes les subtilités du domaine. A terme, le logiciel implémenté modélisera le domaine. Toutefois avant que le logiciel ne devienne le reflet du domaine, il faut le comprendre.

Cette compréhension permettra, dans un premier temps, de modéliser le domaine (c’est-à-dire extraire le modèle). Il n’y a pas de méthode pour modéliser le domaine mais on peut s’aider de dessins, diagrammes, simplement un texte écrit etc… Le plus important est de comprendre le domaine pour le communiquer.

La communication du modèle passe par des entrevues entre experts du domaine, concepteurs logiciels et développeurs.

Un frein à la communication du modèle pourrait être l’utilisation d’un langage spécialisé ou technique de la part des experts ou des développeurs. Ce langage spécialisé tend à rendre la compréhension du domaine compliqué pour chacun des acteurs. Il est donc important de se mettre d’accord sur un vocabulaire commun et compréhensible de tous dont le but ultime est un domaine compréhensible et exploitable.

Ce vocabulaire commun est l’ubiquitous language (i.e. “langage omniprésent”), il permet de:

  • Trouver les concepts clés qui définissent le domaine et ensuite la conception

  • Révéler les expressions utilisées dans le domaine

  • Chercher à résoudre les ambiguités et inconnues

L’ubiquitous language ne s’élabore pas en une seule itération, il est le fruit de plusieurs discutions avec les experts du domaine.

Pour l’élaborer, il faut:

  • Eviter d’utiliser des termes que les experts n’ont pas prononcés.

  • Créer un glossaire contenant les termes utilisés par les experts du domaine de façon à les expliquer.

  • Vérifier qu’un terme est utilisé pour un seul concept.

  • Eviter d’utiliser des termes trop proches de solutions techniques ou de “design patterns”. Ces termes peuvent diriger les développeurs vers une solution particulière et ils peuvent être incompréhensible par les experts du domaine.

  • Les termes de l’ubiquitous language doivent se retrouver dans le code pour qualifier des propriétés, des comportements qui implémentent une partie du domaine et surtout les tests. Si l’ubiquitous language est compris, le code sera plus clair.

  • Les évolutions de l’ubiquitous language peuvent se traduire par des modifications des noms ou comportements utilisés dans le code.

  • Il n’est pas nécessaire d’élaborer un ubiquitous language pour toute l’application. Il est préférable de réserver cette démarche aux parties complexes du domaine.

Objets ou bloc d'objets préconisés

Entité

Les objets entités contiennent une identité:

  • L’identité est identique durant tous les états du logiciel

  • La référence à l’objet est de préférence unique pour assure une certaine cohérence.

  • Il ne devrait pas exister 2 entités avec la même identité sous peine d’avoir un logiciel dans un état incohérent.

  • L’identité peut être un identifiant unique ou une combinaison de plusieurs membres de l’entité.

Value-Object

Ce sont des objets n’ayant pas d’identité:

  • Les value-objects n’ont pas d’identité car ils sont utilisés principalement pour les valeurs de leurs membres.

  • Ces objets peuvent facilement créés ou supprimés car il n’y a pas de nécessité de maintenir une identité.

  • L’absence d’identité permet d’éviter de dégrader les performances durant la création et l’utilisation de ces objets par rapport aux entités.

  • Les value-objects peuvent être partagés

  • Dans le cas de partage de value-objects, il faut qu’ils soient immuables c’est-à-dire qu’on ne puisse pas les modifier durant toute leur durée de vie.

  • Les value-objects peuvent contenir d’autres value-objects.

Service

Lorsqu’on définit l’ubiquitous language, le nom des concept-clés permettent de definir les objets qui seront utilisés. Les verbes utilisés qui sont associés aux noms permettront de définir les comportements de l’objet. Ces comportements seront implémentés directement dans l’objet.

Ainsi, lorsque des comportements ne peuvent être associés à un objet, ils doivent être implémentés en dehors de tout objet, dans un service:

  • L’opération dans le service fait référence à un concept du domaine qui n’appartient pas à une entité ou à un value-object.

  • Un service peut effectuer un traitement sur plusieurs entités ou value-objects.

  • Les opérations n’ont pas d’états.

  • Les services ne doivent pas casser la séparation en couche, ainsi un service doit être spécifique à une couche.

Module

Permet de regrouper les classes pour assurer une cohésion:

  • dans les relations entre les objets

  • dans les fonctionnalités gérées par ces objets.

  • L’intérêt est d’avoir une vue d’ensemble en regardant les modules, on peut ensuite s’intéresser aux relations entre les modules.

Les modules doivent:

  • former un ensemble de concepts cohérents, de façon à réduire le couplage entre les modules.

  • Le couplage faible permet de réduire la complexité et d’avoir des modules sur lesquels on peut réfléchir indépendamment.

  • Etre capable d’évoluer durant la durée de vie du logiciel.

  • Etre nommés suivant des termes de l’ubiquitous language.

Aggregate

Les objets du modèle ont une durée de vie:

  • Ils peuvent être créés, placés en mémoire pour être utilisés puis détruits ensuite.

  • Ils peuvent aussi être persistés en mémoire ou dans une base de données.

La gestion de cette durée de vie n’est pas facile car:

  • Les objets peuvent avoir des relations entre eux : 1 à plusieurs, plusieurs à plusieurs.

  • Il peut exister des contraintes entres les objets au niveau de leur relation : par exemple unidirectionnel ou bidirectionnel.

  • Il peut être nécessaire de maintenir des invariants c’est-à-dire des règles qui sont maintenues même si les données changent.

  • Il faut assurer une cohésion du modèle même dans le cas d’association complexe.

Une méthode est d’utiliser un groupe d’objets comme les agrégats (i.e. “aggregate”). Les agrégats sont des groupes d’objets associés qui sont considérés comme un tout unique vis-à-vis des modifications des données, ainsi:

  • Une frontière sépare l’agrégat du reste des objets du modèle,

  • Chaque agrégat a une racine qui est une entité qui sera le lien entre les objets à l’intérieur et les objets à l’extérieur de l’agrégat.

  • Seule la racine possède une référence vers les autres objets de l’agrégat.

  • L’identité des entités à l’intérieur de l’agrégat doivent être locale et non visible de l’extérieur.

  • La durée de vie des objets de l’agrégat est liée à celle de la racine.

  • La gestion des invariants est plus facile car c’est la racine qui le fait.

  • La racine utilise des références éphémères si elle doit passer des références d’objets internes à des objets externes. L’intégrité de l’agrégat est, ainsi, maintenue.

  • On peut utiliser des copies des value-objects.

Factory

Les fabriques sont inspirées du “design pattern” pour créer des objets complexes:

  • Elles permettent d’éviter que toute la logique de création des objets ne se trouve dans l’agrégat.

  • Permet d’éviter de dupliquer la logique de règles s’appliquant aux relations des objets.

  • Il est plus facile de déléguer à une fabrique la création d’une agrégat de façon atomique.

  • La gestion des identités des entités n’est pas forcément triviale car des objets peuvent être créés à partir de rien, ils peuvent aussi avoir déjà existé (il faut être sûr qu’il n’existe pas encore une autre entité avec le même identifiant) ou il peut être nécessaire d’effectuer des traitements pour récupérer les données de l’entité en base de données par exemple.

L’utilisation de fabriques n’est pas indispensables, on peut privilégier un constructeur simple quand:

  • La construction n’est pas compliquée : pas d’invariants, de contraintes, de relations avec d’autres objets.

  • La création n’implique pas la création d’autres objets et que toutes les données membres soient passées par le constructeur.

  • Il n’y a pas de nécessité de choisir parmi plusieurs implémentations concrètes.

Repository

Dans le cas d’utilisation d’agrégats, si un objet externe veut avoir une référence vers un objet à l’intérieur, il doit passer par la racine et ainsi, avoir une référence vers la racine de l’agrégat, ainsi:

  • Maintenir une liste de références vers des racines d’agrégat peut s’avérer compliqué dans le cas où beaucoup d’objets sont utilisés. Une mise à jour de la référence de la racine auprès de plusieurs objets peut s’avérer couteux.

  • L’accès à des objets de persistance se fait dans la couche infrastructure, les implémentations permettant d’y accéder peuvent se trouver dans plusieurs objets et ainsi être dupliquées.

  • Un objet du modèle ne doit contenir que des logiques du modèle et non les logiques permettant d’accéder à une base de persistance.

Utiliser un repository permet:

  • D’encapsuler la logique permettant d’obtenir des références d’objets.

  • Stocker des objets

  • D’utiliser une stratégie particulière à appliquer pour accéder à un objet.

L’implémentation d’un repository peut se faire dans la couche infrastructure toutefois l’interface de ce repository fait partie du modèle.

Le repository et la fabrique permettent, tout deux, de gérer le cycle de vie des objets du domaine:

  • La fabrique permet de créer les objets

  • Le repository se charge de gérer des objets déjà existants.

Odoo • Image et Texte

Architectures en couche

DDD préconise de séparer le code en couche pour ne pas diluer la logique métier dans plusieurs endroits. Chaque couche a une fonction particulière qui est utilisable par d’autres couches de façon à:

  • Mutualiser le code suivant une logique

  • Éviter la duplication de code métier

Les 4 couches sont:

  • la couche utilisateur,

  • la couche application,

  • la couche domaine,

  • la couche infrastructure

 

Comment préserver le modèle ?

Les gros projets sont généralement composés de plusieurs équipes. Pour que le modèle reste cohérent malgré la division en plusieurs équipes séparées:

  • Chaque partie du projet doit être assignée à une équipe

  • Une modification dans une partie du modèle maintenue par une équipe ne doit pas déstabiliser le modèle en le rendant incohérent.

  • Il faut diviser le domaine en plusieurs modèles, définir des frontières entre les modèles et les liaisons entre eux.

Cette partie indique quelques patterns généraux permettant d’organiser une équipe de développement pour préserver le modèle général.

Bounded Context

Un domaine s’applique implicitement à un contexte particulier. Diviser le domaine en plusieurs sous-domaine implique d’appliquer un contexte différent à chaque sous-domaine. Chaque sous-domaine devient donc limité à un contexte d’où le contexte borné (i.e. “bounded context”). La division en contexte borné est une des étapes les plus importantes dans un projet DDD.

Ainsi:

  • Les sous-modèles doivent être assez petits pour être applicable à une équipe.

  • Le contexte d’un modèle est l’ensemble des conditions qu’on doit appliquer pour s’assurer que les termes utilisés dans le modèle prennent une sens précis.

  • Définir les limites du contexte permet de préserver l’unité du modèle.

  • Il est plus facile de maintenir un modèle quand son périmètre est connu.

  • Il faut bien délimiter le contexte pour éviter des duplications de logique métier si un sous-modèle empiète sur un autre.

  • Les échanges entre sous-domaine peuvent se faire avec des value-objects par exemples.

  • Un contexte englobe la notion de module.

Les divisions en contexte borné peuvent se faire suivant des critères différents:

  • Si une ambiguité est apparue dans l’ubiquitous language ou dans les concepts métier et qu’elle nécessite d’envisager deux contextes différents.

  • Pour être plus en phase avec l’organisation de plusieurs équipes ou leur emplacement physique.

  • Pour qu’un sous-domaine soit lié à sa fonction métier.

  • Intégrer du code “legacy” ou du code tiers.

  • Si plusieurs langages de programmation ou plusieurs technologies sont utilisés.

Un sous-domaine ne correspond pas forcément précisement à un contexte borné. En effet, un sous-domaine résulte d’une séparation fonctionnelle du domaine. Idéalement il faudrait un domain model pour chaque sous-domaine toutefois un sous-domaine peut contenir plusieurs domain models. Un contexte borné correspond à une implémentation concrète et à une séparation technique qui applique des frontières aux objets du domain model. Ainsi, un sous-domaine comporte un ou plusieurs contextes bornés et un contexte borné comporte un ou plusieurs domain models.

Partage de données entre contextes bornés

Le partage d’objets entre contextes bornés peut mener à des incohérences dans le domain model. En effet des objets provenant d’autres contextes bornés peuvent avoir été concus avec un ubiquitous language différent. Il faut donc éviter de partager directement des données entre contexte borné. Les échanges de données doivent se faire avec des DTO ou par l’intermédiaire d’une couche anticorruption.