Dans son livre DDD, Evans promeut l'idée d'architecture en couches, et en particulier que la logique métier doit être confinée à la couche domaine et séparée de l'interface utilisateur/persistance/autres préoccupations. Il introduit également le modèle de référentiel comme moyen d'abstraire l'accès et le stockage persistant aux entités et aux objets de valeur. Pour moi, ce qui suit n'est pas clair:
(Je suis surtout intéressé par la perspective DDD)
Les référentiels et leur placement dans la structure du code est un sujet de débat intense dans les cercles DDD. C'est aussi une question de préférence, et souvent une décision prise en fonction des capacités spécifiques de votre framework et de l'ORM.
Le problème est également confus lorsque l'on considère d'autres philosophies de conception comme l'architecture propre, qui préconisent l'utilisation d'un référentiel abstrait dans la couche domaine tout en fournissant des implémentations concrètes dans la couche infrastructure.
Mais voici ce que j'ai découvert et ce qui a fonctionné pour moi, après avoir essayé différentes combinaisons/permutations.
A vos questions:
À quelle couche les référentiels appartiennent-ils: la couche de domaine, la couche de persistance ou quelque chose au milieu?
Je dirais qu'il y a trois couches distinctes dans les applications DDD - la couche de domaine interne, la couche d'application externe et le monde externe (comprend l'API/UI).
La couche de domaine contient des agrégats, des entités, des objets de valeur, des services de domaine et des événements de domaine. Ces éléments ne dépendent que les uns des autres et évitent activement les dépendances des couches externes.
La couche d'application contient les services d'application, les référentiels, les courtiers de messages et tout ce dont vous avez besoin pour rendre votre application pratiquement possible. Cette couche est l'endroit où se déroule la majeure partie de la persistance, de l'autorisation, du traitement pub-sub, etc. La couche application dépend et connaît les éléments de la couche domaine, mais suit les directives DDD lors de l'accès au modèle de domaine. Par exemple, il n'appellera que des méthodes sur les agrégats et les services de domaine.
La couche la plus externe comprend les contrôleurs API, les sérialiseurs, l'authentification, la journalisation, etc., tout ce qui n'est pas lié à la logique métier ou à votre domaine, mais fait partie intégrante de votre application.
Les entités, les objets de valeur ou les services de domaine peuvent-ils appeler des référentiels?
Les référentiels doivent-ils être abstraits de la technologie de stockage (ce qui serait implicite s'ils appartiennent à la couche de domaine) ou peuvent-ils tirer parti de ces technologies de stockage?
Les référentiels sont orientés vers le domaine, ce qui signifie qu'ils contiennent des méthodes significatives du point de vue du domaine (comme GetAdults()
ou GetMinors()
). Mais la mise en œuvre concrète peut se faire de deux manières:
Les référentiels peuvent-ils contenir une logique métier?
GetAdults()
au lieu de .filter(age > 21)
.Avez-vous appliqué ces contraintes dans la pratique et quel a été l'effet sur la qualité de votre projet?
Si vous vous limitez à utiliser des référentiels uniquement dans les services d'application et contrôlez les transactions à un seul endroit (généralement avec le modèle d'unité de travail), les référentiels sont assez faciles à utiliser. Dans mes projets précédents, j'ai trouvé qu'il était extrêmement utile de restreindre toutes les interactions de base de données aux référentiels au lieu de saupoudrer les méthodes de cycle de vie dans la couche domaine.
Lorsque j'ai appelé des méthodes de cycle de vie (comme save
, update
, etc.) à partir de la couche d'agrégat, je l'ai trouvée extrêmement complexe et difficile à contrôler de manière fiable les transactions ACID.
À quelle couche les référentiels appartiennent-ils: la couche de domaine, la couche de persistance ou quelque chose au milieu?
Si vous croyez à l'inversion de dépendance*, les référentiels sont des contrats définis par la couche application et implémentés par la couche persistance. Le contrat lui-même a une dépendance implicite du modèle de domaine (via l'apparition des entités de domaine dans la signature), et la mise en œuvre d'un référentiel comprendra souvent une dépendance directe de l'usine responsable de la création des instances.
Les entités, les objets de valeur ou les services de domaine peuvent-ils appeler des référentiels?
Idéalement, ils ne devraient pas en avoir besoin. Le modèle de domaine se trouve au cœur de l'oignon ; c'est une représentation pure en mémoire (d'une partie du) domaine.
Vous pouvez parfois voir un service de domaine qui agit comme façade fournit une vue en lecture seule des données mises en cache. Mais écrire dans un référentiel à partir du modèle de domaine est normalement hors limites (les flèches de dépendance ne pointent pas dans cette direction).
Les référentiels doivent-ils être extraits de la technologie de stockage
Idéalement, le contrat de référentiel décrit explicitement les capacités requises par l'application, sans nécessairement vous coupler à une technologie spécifique.
Les référentiels peuvent-ils contenir une logique métier?
Dans sa forme idéale, un référentiel est une façade pour une collection indépendante du domaine. Donc non. Ceci est quelque peu lié à la question précédente; si l'implémentation du référentiel inclut une logique métier importante, il devient d'autant plus difficile de trouver la logique et de reconnaître les implications que la logique a sur votre choix de stockage.
Une partie réelle de la motivation de ces modèles est une tentative de les rendre plus faciles à entretenir - ce qui inclut la possibilité de deviner plus facilement où dans la base de code un concept important va être réalisé.
Avez-vous appliqué ces contraintes dans la pratique et quel a été l'effet sur la qualité de votre projet?
C'est assez subjectif, mais je n'ai pas trouvé beaucoup de joie dans le modèle de référentiel décrit par Evans. Une partie du problème est que les modèles sont très basés sur le Kingdom of Nouns ; un autre est qu'ils semblent être couplés aux idiomes de Java circa 2003.
L'idée est juste - l'application ne devrait pas avoir besoin de connaître les détails de la gestion de l'état mutable. Nous pouvons correctement placer une frontière entre ces concepts. De même, le modèle de domaine doit être indépendant de son hôte d'application, afin que l'hôte puisse être modifié.
Là où les choses ont tendance à s'effondrer, c'est save
. Comment obtenez-vous les données dont vous avez besoin de l'objet et dans votre stockage? Le modèle de domaine l'a, le référentiel en a besoin, donc vous vous retrouvez avec une pollution persistante entrant dans votre modèle de domaine "pur". La pollution peut prendre plusieurs formes - crochets ORM, membres publics, API de sérialisation ...
Supposons que les entités doivent être immuables. Par définition, le référentiel est une émulation d'une collection d'entités en mémoire, une sorte de carte
Très proche. C'est probablement un meilleur ajustement, sémantiquement, si vous pensez en termes de messages , plutôt que entités . Le modèle de domaine et la persistance doivent savoir comment extraire des informations des messages pour faire leur travail. Voir Limites , par Gary Bernhardt
Alternativement, vous pouvez conserver l'idée du référentiel qui stocke les entités mutables (quelque part), en séparant ces entités de la logique métier . Dans cette approche, l'API d'entité est consommée par le modèle de domaine, mais implémentée par le composant de persistance. Cela maintient l'état mutable à la frontière de votre solution, où il appartient.
* Le terme "Dependency Inversion" est aussi mal défini que tous les autres SOLID, et les articles sur Internet tous les perroquets les mêmes phrases fatiguées pour le définir. Aux fins de cette réponse, la dépendance l'inversion peut être définie comme "la transmission à une classe de ses dépendances concrètes via des paramètres constructeurs définis par des interfaces". Voir ici pour un exemple.
Donc, si vous ignorez toutes les discussions sur les "règles de conception d'agrégats" que vous pouvez trouver sur le net et que vous vous concentrez sur la façon dont les agrégats sont décrits dans le livre, ils sont censés être ces ensembles d'objets qui forment une sorte de graphique, avec un limite de conception explicite et un objet sélectionné comme racine. La racine agit comme une interface (façade) avec l'agrégat, elle l'encapsule et est chargée de maintenir la cohérence à l'intérieur de cette frontière. Votre modèle de domaine est alors plus ou moins composé d'un certain nombre de tels agrégats accessibles par traversée. Un autre rôle important des agrégats est de simplifier le réseau de dépendances entre les objets, de sorte que les relations d'agrégats doivent être conçues de manière stratégique.
La question est de savoir comment obtenir les agrégats initiaux - ceux à partir desquels vous commencez la traversée?
Si vous regardez de plus près la façon dont Evans a défini les référentiels, l'accent n'est pas mis sur la persistance (bien qu'en général, ils finissent par aller dans la base de données) - au lieu de cela, ils sont un moyen d'obtenir les racines agrégées "de niveau de surface" initiales, laissant vous commencez là et atteignez les autres par traversée.
Pour citer le Livre bleu:
"Qu'il s'agisse de fournir une traversée ou de dépendre d'une recherche [dans la base de données] devient une décision de conception, en échangeant le découplage de la recherche contre la cohésion de l'association. [...] La bonne combinaison de recherche et d'association rend la conception compréhensible. "
Cela signifie que la structure de dépendance est telle que (1) lorsqu'un chemin d'exécution est entré, un cas d'utilisation1 qui a besoin d'un agrégat doit demander à un référentiel d'en obtenir un, et (2) le référentiel connaît certains agrégats de "niveau de surface", mais le domaine principal ne connaît pas les référentiels.
Il existe une certaine marge de variation en ce qui concerne la façon dont vous organisez cela en couches, mais vous pouvez voir que la structure ci-dessus impose certaines contraintes sur les directions de dépendance à travers et à l'intérieur des couches. Donc, pour répondre à votre question.
- À quelle couche les référentiels appartiennent-ils: la couche de domaine, la couche de persistance ou quelque chose au milieu?
Un référentiel tel que conceptualisé par DDD est une abstraction - il n'est pas lui-même dans la couche de persistance, il encapsule l'accès à la base de données - ce qui signifie qu'il transmet la demande de reconstitution à une passerelle d'accès aux données qui se trouve dans la couche de persistance (soit via la composition, soit via la répartition dynamique ).
"Pour chaque type d'objet nécessitant un accès global, configurez l'accès via une interface globale bien connue. [Les référentiels encapsulent] la technologie de stockage et de requête réelle. Fournissez des REPOSITOIRES uniquement pour les racines AGGREGATE qui ont réellement besoin d'un accès direct."
Ils sont censés être accessibles tout au long de la couche d'application, ce qui semble être un endroit naturel pour eux de résider, mais la mise en œuvre réelle se fera sur plusieurs couches. La raison de l'encapsulation est de garder le code de couche de service d'application simple et découplé du passe-partout lié à la persistance.
(Il semble que s'il était inférieur à la couche de domaine, il violerait le principe de l'architecture en couches, car cela dépend d'un objet de domaine qu'il stocke)
L'abstraction du référentiel n'est pas en dessous de la couche de domaine, mais elle dépend des objets de domaine - c'est tout. Vous lui demandez une racine agrégée en utilisant la langue du modèle de domaine (par exemple, vous lui passez un ID), puis il explique comment transformer cela en une requête de base de données; la base de données renvoie sa représentation, et le référentiel l'utilise ensuite pour reconstituer l'agrégat demandé, et renvoie cela . Au-delà de cela, il ne contient aucune logique métier significative, et il ne contient certainement pas de logique de domaine spécifique (problème). En pratique, cependant, vous voulez essayer d'arranger les choses de manière à éviter des traductions élaborées et coûteuses entre les représentations.
Une autre chose qui est discutée dans le livre est qu'il est souhaitable de trouver un moyen d'exprimer des limites agrégées dans le code lui-même, mais qu'il peut être un peu délicat de le faire. La chose la plus courante que vous trouverez sur le Web est l'idée popularisée par Vaughn Vernon, qui stipule que les agrégats doivent référencer d'autres agrégats exclusivement via leur identité. Cela peut bien fonctionner dans de nombreux cas, et a l'avantage de délimiter clairement les limites agrégées dans le code, mais l'OMI est trop restrictif et quelque peu centré sur la base de données en pensant qu'il soit élevé au statut d'une règle que vous appliquez ou même par défaut à en matière de conception globale. Ne pensez donc pas que c'est la seule option - vous pouvez concevoir un schéma différent.
Si vous suivez ce chemin, vous devez trouver comment traverser les agrégats en fonction de cette restriction spécifique. Vaughn Vernon recommande "d'utiliser un référentiel ou un service de domaine pour rechercher des objets dépendants avant d'invoquer le comportement d'agrégat" (et de ne pas utiliser un référentiel depuis l'agrégat, pour empêcher les agrégats d'avoir une dépendance sur un ou plusieurs référentiels).
Il y a un peu de compromis ici (comme pour chaque décision de conception): une certaine quantité de connaissances sur le fonctionnement interne de l'agrégat est poussée ailleurs, et les signatures des méthodes des agrégats deviennent plus verbeuses (et certaines des dépendances moins évident car ils ne sont pas visibles dans le constructeur).
Il s'agit d'une approche viable, mais il convient de noter qu'elle est un peu différente de ce qui est décrit dans le livre d'Evans - où l'idée était d'être très délibérée sur les racines agrégées qui devraient être obtenues via les référentiels, et celles via la traversée, et sous quelles circonstances - en fonction de votre compréhension du domaine, des processus métier, des modèles d'accès, etc.
1Les deux couches d'intérêt ici, dans la terminologie DDD, sont la couche application et la couche domaine. La couche d'application ("définit les tâches que le logiciel est censé faire et dirige les objets du domaine expressif pour résoudre les problèmes") correspond grosso modo à la couche Application Business Rules (Use Cases) dans Clean Architecture ("use cases [...] ordonner [aux] entités d'utiliser leurs règles commerciales à l'échelle de l'entreprise pour atteindre les objectifs du cas d'utilisation ").
Selon dddsample vous pouvez avoir un référentiel comme interface ( CargoRepository ) dans la couche domaine et une implémentation réelle ( CargoRepositoryHibernate ) dans la couche Infrastructure.
Image d'architecture en couches (également du livre bleu DDD):