Points Clés
- La notion de dépendance est une problématique qui, si ignorée, peut entraîner des complications au niveau de l’architecture en termes d’évolution et de maintenance.
- Afin d’obtenir des solutions maintenables et évolutives le sens des dépendances doit être guidé par la valeur métier.
- L’inversion de dépendance est un outil puissant pouvant être utilisé à différents niveaux d’abstraction afin de pallier le problème de dépendance
- Les dépendances ne sont pas anodines et ont des conséquences pouvant impacter l’organisation.
- L’objectif est de protéger le “core” métier de votre solution pour pouvoir, d’une part lui garantir une évolution rapide en minimisant les impacts, et d’autre part de produire, à moindre coût, le plus de valeur métier possible.
De nos jours, les solutions sont de plus en plus amenées à être construites autour d’un ensemble de composants, distribués ou non. Lorsqu’on démarre, ou travaille sur ces projets, une problématique se pose, qui si ignorée, peut entraîner des complications au niveau de l’architecture en termes d’évolution et de maintenance : la notion de dépendance.
Afin de parler de ce sujet, nous commencerons par expliciter ce que j’entends par dépendance, ainsi que les différents types existants. Une fois la notion clarifiée, nous aborderons le lien important entre dépendance et valeur métier, ainsi que leurs conséquences à différents niveaux : classe, composant et organisationnel.
Définition
Commençons par nous entendre sur une définition de dépendance :
On dit que A dépend de B, si pour que A réalise une action X, la présence de B est requise.
Dans notre contexte de l’informatique A et B peuvent être des solutions, des composants (ex : war, jar,..) ou à plus bas niveau des classes (dans un contexte de programmation objet). Dépendant de l’action X, nous pouvons différencier deux types de dépendances principales.
Si A requiert la présence de B pour compiler, on parlera de dépendance au « build » que l’on notera :
Dans la suite de l’article, lorsqu’on parlera de dépendance, on fera référence à celle de «build». Si A a besoin de B pour exécuter avec succès une fonctionnalité, on parlera de dépendance au «runtime», ou «flow» d’exécution que l’on notera :
Maintenant que nous avons une vision commune de ce qu’est une dépendance, et de leur notation, nous allons observer quelques bonnes pratiques.
Lien entre dépendance et valeur métier
Il est à noter que les bonnes pratiques que l’on peut trouver au niveau du code peuvent souvent s’appliquer à un niveau d’abstraction supérieur. Quand on parle d’une bonne gestion des dépendances, je pense à la «Clean Architecture» d’«Uncle Bob».
Sans rentrer dans les détails de celle-ci, il est à retenir au moins une chose, les couches ayant une moindre valeur métier, dites de détails (frameworks, base de données), dépendent de celles ayant une valeur métier plus forte.
Pourquoi ?
Une dépendance n’est jamais anodine, surtout celles au niveau du «build», car si A dépend de B, alors une évolution de B a de fortes chances d’impacter A.
Dans le cas où B, possède une forte valeur métier, nous consentons au coût potentiel de changer A. Par contre, si B n’a qu’une valeur métier faible par rapport à A, alors le coût est cher payé.
On souhaite dans toutes solutions minimiser les coûts ayant peu de valeurs ajoutées. Pour ce faire, il nous faut limiter, autant que possible, les dépendances vers les composants ayant une faible valeur ajoutée.
La question de comment déterminer la valeur de la fonction «valeurMetier» doit se poser. Une approche orientée DDD (Domain Driven Design) , avec un travail rapproché entre développeurs et experts métiers, peut permettre de déterminer et d’identifier le cœur métier («Core Domain»), des autres («Supporting Domain», ou «Generic SubDomain»), et ainsi obtenir une comparaison pertinente (cf. Visualising Socio-Technical Architecture with DDD and Team Topologies).
Outils
Pour pouvoir éviter ou corriger ce type de dépendance, il faut d’abord en être conscient et pouvoir clairement les représenter.
On peut commencer à identifier les dépendances intercomposants. Pour cela, on se base sur les dépendances de type « build » facilement identifiable depuis, par exemple, le pom avec maven, le code avec les tests de composants, et on réalise un diagramme de composants.
Au niveau du composant en lui-même le besoin est moindre, car on peut se reposer sur le type d’architecture mis en place, qui dans le cas d’une architecture respectant les principes de la «Clean Architecture», comme l’architecture hexagonale, garantira, si respectée, l’obtention d’une bonne gestion des dépendances.
Le problème réside principalement aux niveaux inter composants.
Inversion de dépendances
Prenons le cas d’un projet existant, qui après obtention d’un diagramme de composants, fait apparaître deux services distribués A et B avec :
Nous souhaitons remédier à cela, tout en gardant le flow d'exécution allant de A vers B, car dans l’idéal on voudrait avoir, soit :
- valeurMetier(A) < valeurMetier(B), ce qui n’est pas le cas
- soit dep: B -> A ce qui est pour le coup réalisable.
Ce que nous voulons obtenir est possible via l’utilisation d’un des principes de SOLID : l’inversion de dépendance («Dependency Inversion Principle»).
Nous souhaitons l’appliquer au niveau composant tout comme on le fait naturellement au niveau du code.
Nous allons maintenant observer comment utiliser ce principe à deux niveaux d’abstractions différents, au niveau classe dans un premier temps, et au niveau composant dans un second.
Niveau d’abstraction : Classe
Commençons par illustrer ce principe au niveau d’abstraction de la classe. Prenons pour cela un cas basique pour réaliser une inversion de dépendances. Soit la classe A qui dépend de la classe B, avec un flow d'exécution allant de A vers B, nous souhaitons inverser cette dépendance tout en gardant le sens du flow d'exécution. Il faut :
- Créer une abstraction qui sera une interface dont va dépendre la classe A ;
- Faire implémenter cette interface par la classe B
Ainsi, on obtient bien le fait que d’une part, la classe A ne dépend plus de la classe B, et d’autre part que la classe B dépend de l’interface exposée par A. Au niveau du «flow» d'exécution du code nous avons toujours le sens A vers B, mais au niveau des dépendances on a l’inversion désirée :
Niveau d’abstraction : Composant
Se pose maintenant la question de comment amener ce principe au niveau d’abstraction supérieur, comme les composants. En utilisant le même principe, on peut modéliser l’inversion de dépendances sous la forme d’un diagramme UML de composants.
Le composant A va exposer un contrat d’interface qui sera implémenté par le composant B. Ce qui est intéressant dans ce type de modélisation, c’est qu’on ne présage en rien de la solution d’implémentation de ce contrat d’interface.
Afin de nous intéresser aux implémentations potentielles, positionnons-nous dans le cas le plus courant d’une architecture distribuée.
Comment fait-on dans une architecture distribuée pour qu’un service expose un contrat d’interface qui sera implémenté par un autre composant ?
On s’intéressera dans la suite à 2 solutions : la première via l’utilisation d’un contrat REST et la seconde via l’introduction d’un nouveau composant de type pub/sub.
Utilisation d’un «webservice» (via un contrat REST)
On se place dans les mêmes conditions que pour le niveau d’abstraction de la classe, mais cette fois-ci, A et B seront des services distribués et nous avons toujours le flow d'exécution allant de A vers B :
Nous souhaitons obtenir la configuration suivante :
Il faut bien comprendre qu’ici, il ne s’agit pas de simplement faire exposer à A une API REST et de la faire consommer par B, car sinon on inverserait aussi le «flow» d'exécution (dep : A -> B & flow : A -> B), ce que l’on ne souhaite pas.
Pour ne pas inverser ce «flow» d'exécution, A va devoir définir un contrat d’API REST dont il sera le garant, par exemple en utilisant la spécification OpenAPI qui sera implémenté par B.
Ainsi, A n’a pas de dépendance au build vers B, en étant le responsable du contrat et de son contenu, et B devient dépendant de A via le contrat OpenAPI, on obtient donc l’inversion désirée.
Utilisation d’un composant Pub/Sub
Une autre possibilité pour réaliser une inversion de dépendance au niveau composant est l’introduction d’un composant technique de type pub/sub. Le contrat exposé par A sera la définition du contenu du message, ainsi que le topic permettant de les transmettre.
Finalement, nous obtenons A qui émet des messages, dont elle est responsable, dans un topic dédié. A n’a plus de dépendance envers B, par contre B doit connaître le contenu du message délivré par A et possède ainsi une dépendance envers A.
Nous avons bien le «flow» d’exécution inversé par rapport à la dépendance.
Il est à noter que ce n’est pas seulement parce qu’on utilise un composant de type pub/sub qu’on inverse la dépendance. En effet, il faut de plus que le contenu du message soit de la responsabilité de l'émetteur. Dans le cas inverse, la dépendance n’est pas inversée, car A dépendrait au niveau du build de B, qui aurait défini le message.
Impact organisationnel
Précédemment, nous avons vu quelques techniques permettant d’inverser la dépendance au niveau des classes et des composants. Si maintenant nous prenons un peu de recul, et de hauteur, pour se poser la question suivante : est-ce que ces changements ont un impact au niveau organisationnel ? La réponse est bien évidemment oui, car derrière chaque composant, il y a de fortes chances d’avoir une équipe, et donc quand on inverse une dépendance on inverse aussi la dépendance d’une équipe envers l’autre. L’organisation se met donc à évoluer naturellement pour s’accorder avec la nouvelle architecture liée à ces dépendances inversées (cf. Loi de Conway).
Avec toujours en tête l’objectif d’avoir le moins de dépendances vers le/les composant(s) ayant le moins de valeur métier pour l’entreprise, on se retrouve naturellement avec une organisation qui tend vers une étoile. Elle sera bâtie autour du domaine apportant une valeur métier ayant un atout business différenciant et le «ROI» le plus important, ce que l’on appelle dans le DDD : le «core domain».
Le fait d’avoir le «core domain» au centre, et en périphérie les composants des domaines ayant une moindre valeur métier, permet une meilleure évolution de l’architecture.
On met ainsi l’emphase sur l'obtention d’un «core domain» extrêmement résistant aux changements périphériques et concentré seulement sur ce qui est différenciant pour l’entreprise.
Ce n’est pas parce que le «core domain» se retrouve au centre, que les relations entre «core» et les autres seront dictées par celui-ci. En effet, dans les patterns «strategic» du DDD plusieurs types de relation sont présentés afin de décrire les relations entre contextes, et donc équipes. On peut citer en exemple la plus contraignante, du point de vue du consommateur, la relation dite «conformist», et une qui l’est moins la «partnership».
La relation «conformist» sera gérée unilatéralement par le fournisseur des informations («supplier») et ne prendra pas en compte les besoins de ceux qui les consomment («customer»).
À l'inverse, la relation de «partnership» implique que les équipes s’alignent sur un ensemble d’objectifs. Il est établi que si l’une des équipes échoue les deux échouent, ce qui implique une plus grande écoute de la part du «supplier». Ces types de relation peuvent être représentées dans ce que l’on nomme un «context mapping».
Conclusion
Nous avons vu dans cet article, qu’il était important de se donner comme objectif d’éviter d’avoir des dépendances (à tous les niveaux d'abstraction) vers ceux (classes, composants) ayant une valeur métier plus faible. L’objectif étant de protéger le «core» métier de votre solution pour pouvoir, d’une part lui garantir une évolution rapide en minimisant les impacts, et d’autre part de produire, à moindre coût, le plus de valeur métier possible.
Nous avons montré qu’avec le principe de SOLID, la «dependency inversion», appliqué au niveau du code et composant, nous étions capables de «refactorer» l’architecture afin d’aligner les dépendances dans le «bon» sens. Pour terminer, nous avons pu constater que ces changements ont des impacts au niveau organisationnel et amèneront à de nouvelles relations entre les équipes responsables de ces composants.
Il est donc important de comprendre, lorsqu’on ajoute des composants dans nos solutions, qu’une dépendance n’est jamais anodine. Si ajoutée sans réflexion, ce choix peut avoir des impacts dans le code, la solution et dans l’organisation à court, moyen et long terme donc soyons consciencieux sur ce type de choix.
A propos de l'auteur
Avec plus de 10 années d'expériences acquises dans le monde Java/JEE, en France et au Canada, sur des projets agiles en tant que développeur, leader technique, architecte et concepteur dans diverses domaines (médical, ferroviaire, bancaire), et étant un adepte de l’amélioration continue, Simon est une personne passionnée qui sait s’adapter et aime relever des défis tant au niveau technique que fonctionnel. Ses sujets d'intérêt préférentiels sont orientés autour de l’amélioration de la qualité logicielle délivrée par les équipes (Agilité, Software Craftsman, Clean Architecture, TDD, DDD, team topologies).