Services, dépôts et fabriques
VoletTactique Les trois patrons de soutien : où loger la logique qui n’appartient à aucune entité, comment récupérer un agrégat, et comment en fabriquer un.
Service de domaine
Un service de domaine porte une logique qui appartient bel et bien au domaine, mais à aucune entité ni objet valeur en particulier. Deux cas de figure, et deux seulement :
- l’opération concerne plusieurs agrégats — un transfert entre deux comptes, la vérification d’une règle qui croise une réservation et une séance ;
- l’opération est une politique dont le domaine parle comme d’une chose en soi : un calcul de tarif, une stratégie d’allocation, un moteur de scoring.
Trois propriétés le caractérisent : il est sans état, il est nommé avec les mots du métier, et il ne dépend d’aucune infrastructure. Un service de domaine qui prend un client HTTP en paramètre n’est pas un service de domaine.
Warning
Le service de domaine est le patron le plus détourné de DDD. Il est tellement commode qu’il devient l’endroit où l’on met tout ce qu’on n’a pas su placer ailleurs — et l’on obtient le modèle anémique : des entités réduites à des accesseurs, et des services de mille lignes qui les manipulent de l’extérieur. La règle de priorité est stricte : d’abord l’objet valeur, ensuite l’entité, le service en dernier recours.
Service de domaine ou service applicatif ?
La confusion la plus fréquente, et elle a des conséquences concrètes puisque les deux ne vivent pas au même endroit.
| Service de domaine | Service applicatif (cas d’usage) | |
|---|---|---|
| Contient | Une règle métier | Un enchaînement, aucune règle |
| Sait ce qu’est | Un tarif, une allocation | Une transaction, un utilisateur connecté |
| Dépend de | Rien | Des ports : dépôts, notifications, horloge |
| Emplacement | Au cœur du domaine | À la frontière — l’application de l’hexagone |
| Nom typique | CalculateurDeTarif, AllocateurDePlaces | ConfirmerReservation, ValiderCommande |
| Test | Pur, aucune infrastructure | Avec des adaptateurs en mémoire |
Le service applicatif est le ValiderCommande de la page
hexagonale : il charge
un agrégat, appelle une méthode dessus, sauvegarde, publie. S’il contient un
if métier, la règle est au mauvais endroit.
Dépôt
Un dépôt (repository) donne l’illusion d’une collection en mémoire d’agrégats. On y met une racine, on en récupère une, on en retire une. Toute la mécanique de stockage disparaît derrière cette illusion.
Trois règles suffisent à ne pas se tromper.
Un dépôt par racine d’agrégat, et pas un par table ni par entité. Il n’existe
pas de LigneReservationRepository : les lignes sont à l’intérieur de l’agrégat, on
y accède par la racine.
L’interface appartient au domaine. C’est un port au sens de l’architecture hexagonale : déclaré dans le domaine, implémenté dans l’infrastructure. C’est le principe d’inversion des dépendances appliqué à la persistance.
Il manipule des agrégats entiers, pas des fragments. Pas de méthode qui retourne trois colonnes.
Ce qu’un dépôt ne doit pas devenir
Un DAO à quarante méthodes. parClientEtDateEtStatutTrieParMontant est le
symptôme d’un besoin de lecture déguisé en besoin de domaine. Les écrans, les
exports et les tableaux de bord n’ont pas à passer par les agrégats : une requête
dédiée, qui lit directement et renvoie exactement ce qu’affiche la page, est plus
simple, plus rapide et ne déforme pas le modèle. C’est la séparation
lecture/écriture, dont CQRS est la formalisation.
Un endroit où mettre des règles. Un dépôt ne décide rien. S’il contient un if
métier, la règle a fui hors du domaine.
Tip
Deux styles de dépôt cohabitent. Le style orienté collection imite un
ensemble — l’agrégat modifié est enregistré automatiquement par l’unité de travail
de l’ORM, sans appel explicite. Le style orienté persistance exige un
enregistrer() explicite. Le second est plus verbeux et bien plus prévisible : on
voit dans le code du cas d’usage où la transaction est validée. C’est le seul
possible sans ORM à suivi de changements — donc le seul en Rust, et le plus sûr
ailleurs.
Fabrique
Une fabrique encapsule une construction assez complexe pour être un sujet en soi. Le besoin est réel dans trois cas : l’assemblage exige des règles qui n’appartiennent pas à l’objet construit, la construction dépend de plusieurs sources, ou la reconstitution depuis la persistance doit contourner les règles de création.
Ce dernier point est le plus concret. Créer une réservation impose de vérifier des règles ; la recharger depuis la base ne doit rien vérifier — elle a déjà existé, et refuser de la charger parce qu’une règle a changé depuis rendrait les anciennes données inatteignables. Deux chemins de construction sont donc nécessaires.
Note
Dans la grande majorité des cas, une méthode de fabrique statique sur l’agrégat
suffit, comme ci-dessus, et une classe ReservationFactory séparée serait une
cérémonie inutile. La fabrique dédiée se justifie quand la construction dépend de
choses que l’agrégat n’a pas à connaître — un service de tarification, un
référentiel externe — ou quand elle doit choisir entre plusieurs types concrets,
auquel cas on retrouve le
Factory Method du GoF.
Récapitulatif
| Patron | Rôle | Signe qu’il est mal employé |
|---|---|---|
| Service de domaine | Une règle qui croise plusieurs agrégats | Il contient toute la logique et les entités sont vides |
| Service applicatif | Orchestrer un cas d’usage | Il contient un if métier |
| Dépôt | Illusion d’une collection d’agrégats | Quarante méthodes de recherche, ou un if métier |
| Fabrique | Construction complexe, reconstitution | Une classe dédiée pour un new à deux arguments |
Références
- Eric Evans, Domain-Driven Design, chapitres 5 et 6 — Services, Repositories, Factories
- https://martinfowler.com/eaaCatalog/repository.html
- Vaughn Vernon, Implementing Domain-Driven Design, chapitres 7 et 12