Dans la première partie nous avons vu à quoi servaient le DDD, le nommage des classes, découper son projet en Bounded Context et comment les Aggregates peuvent séparer les responsabilités.
Comment faire pour éviter que notre projet ne se transforme en une usine à gaz ?
S’il y avait une règle à retenir ça serait celle-ci :
Un cas d’utilisation = une transaction = un aggregate
Symfony et Doctrine2 nous offrent beaucoup de facilités et dans la plupart des projets, cela convient très bien. Nos entités sont référencées entre elles par l’ORM et le développement va très vite. Tellement vite qu’un jour on se dit qu’on a construit une grosse usine incompréhensible pour un développeur extérieur au projet. Et là commencent les galères des évolutions et des bugs, ce qu’on appelle aussi la “Dette Technique”.
Certains découpent les projets en plusieurs bundles : CoreBundle, UserBundle, ProductBundle, CartBundle, MediaBundle…C’est très bien, c’est joli. Dans les best practices de Symfony, ils proposent de créer un seul AppBundle et de tout mettre à l’intérieur. Pour de petits projets pourquoi pas. Pour de plus gros, c’est particulièrement regrettable car beaucoup de développeurs pensent qu’il est judicieux de rassembler tout leur code métier dans un seul système.
Au final vous pourriez vous retrouver avec votre site e-commerce, complet et qui satisfera les besoins de chacun. C’est trompeur, car peu importe combien de problèmes vous allez gérer dans votre projet, jamais il ne répondra à toutes les exigences potentielles du consommateur. Ajouter à cela le fait de ne pas séparer vos models/entités, le projet sera difficilement maintenable lors des mises à jour, fortement couplé et dépendra de tout le reste.
Prenons un exemple de code, on veut démarrer une discussion sur un Forum :
$user->getPerson()->getName()->getFormattedName(). Pas très propre au passage.
Cet exemple est clairement un mauvais design. On ne devrait pas avoir de référence à User, encore moins à Permission. Une mauvaise conception conduit vers ce genre de chose. Un œil sur $author, et on comprend que le développeur, au lieu de créer un simple ValueObject Author, semble satisfait de gérer l’auteur via trois accès de données différents. C’est loin d’être un cas isolé.
Si on ne pose pas de limites, c’est sûr qu’un simple AppBundle suffit. Pourtant un Forum n’est pas obligatoirement un espace avec des permissions, au contraire dans notre Forum (Bounded) Context on devrait simplement s’occuper de savoir comment démarrer une discussion.
La gestion des permissions peut se faire dans un Access (Bounded) Context séparé. On peut très bien créer une API REST pour obtenir les rôles d’un utilisateur sous forme de réponse XML ou JSON en soumettant l’ID de l’utilisateur et le rôle souhaité.
Le Forum Context utilise de son côté un ACL (anticorruption layer) pour communiquer avec l’Access Context. Le JSON reçu est transformé en véritable utilisateur Value Object dans le Forum Context.
Utiliser un ACL permets de ne pas polluer notre Forum Context, on récupère quelques informations externes et on les transforme pour les adapter à notre domaine.
Ça demande quelques petits efforts de plus pour l’Access Context, mais les bénéfices sur le long terme en terme de maintenance et de clarté du code justifient cette implémentation.
Synchronisation
On récupère quelques informations sur les permissions de l’utilisateur, par exemple on récupère son ID, son pseudo et son rôle. On stock le tout dans notre base, ça implique de copier des informations pour ne pas avoir à les récupérer tout le temps.
On a donc besoin d’un système de synchronisation entre nos deux Bounded Contexts. Un moyen possible est de proposer des urls sur Access Context :
/access/notifications /access/notifications/{notificationId}
La première renverra la page courante des notifications. Une notification est en fait un Domain Event.
Domain Event
Un Domain Event en DDD représente un état d’un objet à un instant t résultant d’une action donnée.
Pour être clair, nos objets ont un état courant, par exemple l’action d’assigner un rôle à un utilisateur AssignRoleToUser, va produire un Domain Event RoleAssignedToUser. Si on stock cet event, on peut deviner son état actuel. En enchaînant les Domain Event et en les stockant, on peut retracer l’historique de ses états et obtenir l’état courant :
RoleAssignedToUser(user_id, username, role_name, date) UsernameChanged(user_id, username, old_username, date)
Certains reconnaîtront en javascript la librairie Redux basée sur ce fonctionnement. C’est le même principe, une action sur un état produit un nouvel état.
f(action, current_state) => new_state.
Pour revenir aux urls, on peut imaginer le Forum Context consommant ces urls et mettant à jour eux mêmes leurs informations. La seconde url est utile si on veut récupérer un état précis, on peut également ajouter de la pagination pour récupérer tout l’historique si nécessaire.
Je suis bien conscient que les Domain Event ont été survolés rapidement, on y reviendra dans une autre partie.
Pause
Faites une pause pour assimiler tout ça si besoin !
On vient de voir comment séparer nos models et comment communiquer entre eux. Lorsqu’on se lance dans le DDD et ce genre de modélisation, la difficulté première c’est le changement. Changer les habitudes d’un développeur pour passer du classique site web Symfony à du DDD est un gros challenge. Parce qu’on va devoir lui dire de penser différemment et de coder différemment.
Puis il y a le problème de la communication asynchrone. Faire des APIs c’est pratique, mais on n’est pas obligé de créer un microservice hébergé sur son propre serveur avec sa propre base de données. Il faut que ça ait du sens pour en arriver à cette extrémité. Le Access Context peut très bien être exécuté dans un autre module du projet et partager la base de données courante.
Alors est-ce qu’on a déplacé un problème vers une autre source de problème, et qui ajoute beaucoup plus de code ?
Ne soyez pas si empressé de rejeter ce style. Dans certaines circonstances, ajouter de la complexité est justifiable. DDD ne doit pas être abordé comme une nouvelle ligne sympa à ajouter dans votre CV, c’est un moyen pour résoudre des problèmes spécifiques.
Pour de simple projet un AppBundle est suffisant. Architecturer une grosse application sur le long terme c’est là où le DDD commence à être intéressant.
Lecture :
DDD in PHP
Implementing DDD
Microservices: It’s not (only) the size that matters, it’s (also) how you use them