Constat
J'ai la chance de pratiquer la modélisation d'objets dans le cadre d'applications variées, utilisant pour la peine un mix entre diagrammes UML, diagrammes C4 plus récemment et beaucoup de Design Emergeant en codant directement les classes d'objets au fur et à mesure avec la sécurité du TDD (Test Driven Development).
Et immanquablement se pose la question de : "comment organiser les classes?" et surtout "quelles sont leur responsabilités?". Cela abouti toujours à de grands débats qui animent les séances de code; en cela le Pair Programming, et encore plus le Mob Programming ouvrent les discussions.
Donnant actuellement des cours UML et conception objet à des universitaires, il est toujours délicat d'expliquer comment poser les bases d'un "bon" design objet.
L'apprentissage classique et le nombre de tutos qui ont emboité le pas dans la catégorie "auto formation" se délectent d'exemples simplistes et -hélas- peu en phase avec le quotidien du développeur: le cercle, le carré, le rectangle, la forme géométrique (shape) sont les modèles usés et usuels pour illustrer aux étudiants un 'bon' modèle d'objets. Objets auxquels on colle les opérations fadasses: calculer aire et je ne sais quoi de trop scolaire.
Avec cela, beaucoup de développeurs se font une très mauvaise idée de ce que devrait être un vrai modèle objets dans une application d'entreprise qui se verra mise en production avec des milliers d'utilisateurs (ou plus j'espère) et des années de durée de vie, entrainant une nécessaire maintenance et évolution.
Sans parler des bugs qui apparaitront inévitablement.
L'héritage lancinant de Merise
Je trouve que la méthode Merise a causé énormément de tort et de retard à l'ingénierie logicielle en France (pour ce que j'en connais).
Très en vogue dans les années 80, elle est assez rigide et s'accorde bien du cycle de développement en V. Elle impose tout un tas de diagrammes que je qualifie d'emblée de Big Design Upfront. Ce qui a tendance à rassurer les chefs de projets et les développeurs quand il faut s'attaquer à développer un système sans trop savoir où cela va nous mener.
Cette méthode de modélisation ne prône pas vraiment l'agilité, et, pour moi, mène à des architectures d'applications trop rigides et fortement couplées, avec une qualité de design assez pauvre.
Elle se base aussi sur un postulat bien encombrant: tout doit reposer sur une base de données, relationnelle qui plus est!
Cette omniprésence de la base de données (et des gens qui en sont les gardiens, à savoir les concepteurs et les administrateurs, les fameux DBA longtemps très recherchés et considérés comme des demi-dieux) nous avait amené à ériger en modèle de programmation parfait l'architecture N-tiers, dans laquelle la couche de persistance (l'accès à la base de données) était la fondation de tout et dictait son modèle aux autres couches. D'horribles dépendances se créaient. Mais après tout c'était du Merise à la lettre.
Evidemment cette dépendance au choix d'un SGBDR et un tel couplage était rapidement source d'un coût de maintenance insupportable. Un changement de solution SGBDR demandait quasiment la ré-écriture du tout.
Si malgré tout, on pouvait obtenir l'assurance que l'on allait rester fidèle à son moteur SQL préféré pour la vie, se posait le problème de la responsabilité attribuée: la base allait même jusqu'à gérer les règles métiers complexes;
avec l'échouage ultime qu'était l'utilisation de triggers, où le comportement était mêlé de manière inextricable au simple stockage. Dur à tester, couteux à maintenir, horrible à faire évoluer.
Poussée du design objet
Heureusement les librairies et les frameworks (🤔) nous ont poussé vers d'autres design d'architecture.
Le problème étant: comment faire vivre des objets à coté d'une base de données. Et comment y accéder depuis différentes Interfaces Homme-Machine; ces dernière se multipliant à la faveur du déploiement des Internets et des clients (programmes client/navigateurs/plug-ins) de plus en plus universels et versatiles (pour ordis, tablettes, mobiles, interface vocale, objets connectés).
Cela a donné le genre de modélisation objet où l'on fait émerger les classes parce qu'il y a des tables dans une base de données et on les affubles de toutes les opérations possibles et imaginables:
On avait envie, par principe DRY (Don't Repeat Yourself) d'avoir des objets omnipotents, qui encapsulent tout, le savoir être (l'état) et le savoir faire (les opérations). Des objets avec des tonnes de méthodes. Et des objets qui se promèneraient (les mêmes) de couches en couches du système, de l'interface graphique à la base de données.
Des initiatives ultra-monolithiques furent légion, telles que des bases de données orientées objets (je pense à un vieux système français aujourd'hui disparu: O2) ou des frameworks tout en un, type WinDev (encore hélas en vie) ou FoxPro ou Delphi. Cela pouvait bien fonctionner sur des applis Client Lourd. Mais au prix d'un couplage immense.
Heureusement les interfaces Web et la multiplicité des solutions de stockage (sans parler des services et de la virtualisation extrême de tout cela) rend impossible de trimbaler des objets aussi lourds et chargés de tant de responsabilité.
Et tant mieux, il nous fallait un code plus simple.
Epurer, jusqu'à l'anémie
C'est comme cela qu'on en est arrivé à des modèles d'objets anémiques.
On s'est mis à concevoir des classes qui représentent des entités mais qui ne contiennent aucune logique métier, car elles étaient bien souvent une émanation des DAO (Data Access Objets) dans une architecture 3-tiers ou dans un design MVC/MVVM.
C'était pour "simplifier". Seulement, on n'a fait que mettre la poussière sous le tapis.
Soit la logique métier remontait dans d'énormes classes de "service" ou "business layers" qui finissaient par devenir énormes parce que le métier n'était pas mieux découpé.
Ce qui, au final, n'a rien simplifié car on s'est retrouvé pieds et points liés à la couche d'accès aux données 1.
Avec derrière tout cela, l'idée qu'une bonne couche de liaison à la base de données allait une fois de plus nous dicter tout ce qu'il fallait: fais ton MCD (ou plutôt obéit à celui que le DBA a fait pour toi) et adapte ton code à cela.
Les ORM (Object-relational Mappers) continuaient de nous laisser croire que la méthode Merise était la bonne et que jamais nous ne nous serions capable de nous délivrer de ce fichu MCD.
Ou alors, ces objets orientés "données", non contents d'être anémiques en eux même, pouvaient cacher des règles métiers dans la couche DAO sous jacente: il fallait alors que l'exécution du code traverse la partie DAO pour voir apparaitre les contraintes sur les relations, les formats de données, et une partie des règles métiers que l'on savait traduire en ordre SQL: clés uniques, ou pire: triggers.
Et d'un autre coté, on en venait à coder dans la business layer (et parfois même coté client avec de beaux validateurs 🤢) les même règles.
Dès qu'on multipliait les services, on multipliait les règles, même si plusieurs services pouvaient finalement modifier la même entité (ou agrégat tant qu'on y est) avec chacun ses vérifications pour ses propres besoins, laissant la persistance finale se débrouiller plus ou moins bien avec des injonctions paradoxales. Bugs en pagaille assurés.
Une fois de plus, la base de données avait le dernier mot. Et les DBA jouaient le rôle d'arbitres entre équipes de devs qui se déchiraient sur la compréhension du besoin du client.
C'est du vécu!
Un peu de code ...
Je me suis remis dans la peau du codeur qui chasse les objets anémiés avec un petit Kata pour illustrer le principe Tell Don't Ask avec un TyrePressure Monitor ou un Cpu Monitor (en bref, un moniteur de ce que l'on veut, ca marche très bien).
La règle métier va se loger, non pas dans le service, mais dans la classe moniteur qui est utilisée par le service, afin que cette dernière 'affirme' (tell) la règle au lieu que ce soit le service qui demande (ask) des valeurs internes au moniteur pour satisfaire la règle.
Il faut que toute logique (fonction) qui a besoin d'une donnée doit se trouver dans la même classe qui contient cette donnée (encapsulation et Loi de Demeter).
Le résultat est ici.
Things that change together should be together. (Fowler)
Et si on parlait de DDD ?
Mais comment éviter un domaine objet anémique sans retourner aux erreurs du modèle objet old school, des objets obèses ?
C'est à la lecture de cet article que j'ai eu envie d'écrire ces lignes.
L'idée étant de pouvoir décrire un modèle riche mais affranchi des contraintes qui ne sont pas les siennes:
se sauvegarder dans une base de données untel ou s'afficher à l'utilisateur sur un interface Z (il existe des milliers de frameworks pour faire le beau sur le Web2)) ne concerne en rien, absolument en rien le Modèle du Domaine d'une App ou d'un SI.
Au début était le verbe
La motivation d'Eric Evans, avec son livre bleu Domain Driven Design, n'était pas tant de parler architecture logicielle mais de représentation.
Plutôt que le mot objet/classe, il choisit le mot entité.
Et le plus important des principes qu'il a voulu mettre en avant est celui du langage parlé par les humains impliqués dans un projet.
Pas le langage de développement mais bien le vocabulaire partagé, le nommage des choses, la sémantique commune, la conversation que les développeurs doivent avoir avec les experts métiers.
Cette approche appelée "Ubiquitous Language" nous oblige à poser plusieurs choses.
D'abord séparer les Domaines métiers, car plusieurs se cachent forcément dans tout système d'informations (mot valise que je vais utiliser pour parler d'une App, d'un SaaS, d'un site, d'un prog, bref... du code en production).
Ensuite établir des Entités regroupées en Agrégats afin d'obtenir un Modèle à l'intérieur d'un Domaine Délimité (Bounded Context).
Ce qui émerge petit à petit
Un atelier tel que l'Event Storming ou l'Event Modeling, nous aide à mieux savoir quoi modéliser (et comment) en partant de zéro et sans connaissance technique particulière (des posts its , des stylos et de grands murs suffisent).
Traditionnellement, il y a plusieurs choses qui émergent de ces ateliers: les évènements en premier, les commandes (ou actions en second) et très vite derrière les agrégats et leur "policies".
Cette distinction entre évènements et agrégats n'est pas anodine. Bien que les évènements aient des effets sur les agrégats, les agrégats existent pour eux même. Ils décrivent ce qui reste une fois les évènements passés. Et leur "policies" est leur profession de foi, ce qui est toujours vrai pour eux (et leur entourage proche).
Voila pourquoi une bonne modélisation du Domaine en Agrégats (Entités + Value Objects) va pouvoir nous enrichir en "règles métier", tout en traitant par ailleurs les évènements.
(Pour en savoir plus sur l'Event Storming il y a foule d'articles et bien sûr de publications à ce sujet, mais peu en français.)
Les évènements sont transients. Parfois contingents. Mais ils passent. On ne peut les arrêter. L'Event Sourcing tente de capturer ces évènements, comme on tenterait de capturer le temps.
Ce qui n'est pas sans risque!
On peut avec un certains nombres d'outils, remonter le temps et le cours des évènements; c'est la force de l'Event Sourcing. Au regard d'un certain prix à payer pour y arriver correctement.
Le Domain Model , quant à lui, survit aux évènements; c'est lui que l'on présente à l'utilisateur final (sous une forme adaptée, dite View Model ou Read Model ), lequel se moque pas mal des journaux de logs.
Les éléments du modèle montrent l'état (partiel et à un instant t) du monde (tel que manipulé dans un contexte donné) et sa cohésion.
L'intérêt d'un Modèle bien fait est d'imposer une cohérence qui s'affranchit des évènements (et également de se laisser facilement parcourir, découvrir).
J'aurai le plaisir de vous parler des évènements dans une autre article.
Et avant cela, d'aller plus loin dans les détails de la modélisation objet d'un domaine avec la suite de cet article.
-
On peut encore trouver du code assez horrible comme celui ci (projet Jigsaw), où on vous impose des trucs du genre: As you can see, the Java Beans are strictly equivalent to the table they represents. The name of the bean properties MUST be strictly equals to the column names in the SQL table. ↩
-
Si un framework de présentation (je pense à tous ceux basés sur les patterns MMVM ou MVP ou MVC) vous demande de placer dans sa couche à lui (son environnement si vous préférez) le modèle, je pense que vous pouvez le jeter par la fenêtre. Le modèle métier n'appartient pas à la couche de présentation. La couche de présentation manipule un View Model seulement. Les dépendances viennent du centre (core) de l'architecture, pas des bords (UI ou Storage). ↩
Top comments (2)
Merci pour cet article très intéressant. Sur l'exemple de code (branche CpuMonitorKata), je serais curieux de voir comment vous testez l'entité ConnectedCpuMonitor. Il faudrait peut-être injecter une dépendance à un service "récupérer la température d'un CPU" plutôt que de l'inclure.
Au passage, l'exemple montre que l'on écrit 2 fois la même règle métier "hasAlert" dans les classes CpuMonitor et ConnectedCPUMonitor, alors qu'elles ne diffèrent que par la façon dont elles récupèrent la température qu'elles surveillent.
Qu'en pensez-vous ?
Tout à fait. Le code que j'ai mis dans mon Repo n'est qu'une base pour mener des Kata; pas une solution en lui même.
Et je ne voulais pas parler de suite des DI ^^
C'était juste de quoi lancer la discussion... vous pouvez faire une PR pour des idées d'améliorations ;)