Vous souhaitez nous soutenir ? Devenez sponsor de l'association sur notre page Github
Tutoriels

Des dépendances stables pour une architecture de qualité

Mathieu De Gracia avatar
Publié le 29 octobre 2024
Couverture de l'article Des dépendances stables pour une architecture de qualité
  1. Qu'est-ce que la stabilité
  2. Qu’est-ce qu’un composant stable
  3. Qu’est-ce qu’un composant instable
  4. La stabilité dans les arbres de dépendances
  5. Comment mesurer la stabilité

Défini par Robert C. Martin dans son ouvrage sur la Clean Architecture, le principe de dépendance stable (SDP) déclare que les composants d'une application doivent toujours dépendre de composants plus stables qu'eux.

Suivre ce principe doit nous permettre de construire des applications à la fois plus maintenables et évolutives dans le temps.

Dans cet article, découvrons comment obtenir des dépendances stables entre nos composants afin d'améliorer l'architecture de nos projets !

Qu'est-ce que la stabilité

La stabilité d’une classe se détermine en calculant le nombre de dépendances entrantes et sortantes de la class, nous distinguons principalement deux métriques nous permettant d'obtenir une note de stabilité.

La première de ces métriques, l'afferent coupling (AC), représente le nombre de composants qui dépendent d'un composant donné. Dans cet exemple, le composant Pneu est utilisé par 2 autres composants (Voiture, Camion) son afferent coupling est donc de 2.

La seconde métrique, l'efferent coupling (EC), compte le nombre de dépendances d'un composant donné. Dans cet exemple, le composant Voiture a connaissance des composants Pneu, Porte et Roue. Son efferent coupling est donc de 3.

La métrique de stabilité est une formule se reposant sur ces deux valeurs et donnant un résultat compris entre 0 et 1 : 0 correspondant à une classe parfaitement stable et 1 à une classe parfaitement instable.

stability = EC / (AC + EC)

Analysons les 2 précédents exemples à l'aide de notre formule :

Attention, cette note de stabilité n'est pas une mesure de qualité. Un composant instable n'est fondamentalement pas mauvais et il est même impossible de concevoir une application constituée uniquement de composants stables ... les composants instables offrent une souplesse que les composants stables n'ont pas.

Le composant Pneu est extrêmement stable car il est utilisé par de nombreux autres composants, cette stabilité provient donc de la dépendance de notre projet envers le composant Pneu : le Pneu est stable car il est important et essentiel, vous pouvez voir la stabilité comme la criticité d'un composant par rapport aux autres composants.

Le composant Voiture est quant à lui instable car il est dépendant des autres pour fonctionner, il est donc plus vulnérable aux effets de bord mais également plus facile à modifier car peu d'autres composants sont dépendants de ce dernier.

Ces métriques doivent vous aider à mieux comprendre les interactions qui s’opèrent dans votre application et à vous assurer que votre architecture ne souffre pas de dépendances problématiques.

Voyons désormais ce qu’implique la stabilité des composants et les conclusions que l’on peut en tirer.

Qu’est-ce qu’un composant stable

Un composant devient stable quand il est fortement utilisé par le reste de l’application tout en ne possédant lui même qu’un faible nombre de dépendances.

Son afferent coupling est donc fort et son efferent coupling faible.

Prenons l'exemple d'un composant ArrayHelper offrant des méthodes facilitant la manipulation et le découpage des tableaux, c'est une classe utilitaire très fortement utilisée dans le reste de l'application et elle ne possède elle même aucune dépendance, uniquement du PHP brut.

Son afferent est donc élevé (il est fortement utilisé) et son efferent très faible (il est peu dépendant des autres) cela amène notre classe à avoir une très forte stabilité, que pouvons nous en conclure ?

Une classe de cette stabilité est critique pour notre application et une anomalie pourrait facilement se répercuter sur une large section de fonctionnalités, modifier cette classe devient donc dangereux car vous devez vous assurer que vous n'insérerez pas de régressions

Vous ne voulez pas prendre le risque d'insérer des bugs dans un composant stable car les répercussions pourraient être dangereuses pour la stabilité de votre application : un composant stable se doit donc d'être de qualité, testé intelligemment, de manière exhaustive et enfin détenir peu de responsabilités.

Qu’est-ce qu’un composant instable

Un composant devient instable lorsqu'il possède beaucoup de dépendances tout en ne possédant lui même que peu de réutilisations par d'autres composants.

Son afferent coupling est donc faible et son efferent coupling fort.

Prenons l'exemple d'un composant RegisterUserAction permettant d'enregistrer un nouveau utilisateur en base de données.

Ce composant est utilisé depuis un contrôleur mais possède de nombreuses dépendances avec des services, des repositories, des API ... Toutes ces dépendances sont essentielles car une action contient un cas d'utilisation de notre application et, par nature, de nombreux besoins.

Son afferent coupling est faible (il est utilisé par une unique autre classe), mais son efferent coupling est très fort (il est dépendant de nombreuses classes pour fonctionner), cette situation génère une classe avec une très forte instabilité, proche de 1.

Une classe instable pourrait paraître comme dangereuse voir même une mauvaise idée ... elles sont pourtant essentielles !

Pouvons nous réellement nous passer de composants contenant de la logique applicative (savoir comment enregistrer un utilisateur) dans une application ? Cela est tout simplement impossible.

Les classes instables offrent cette souplesse de modification car elles orchestrent les entrées et sorties d'autres composants sans souvent détenir elle même de logiques métiers : les classes instables détiennent bien souvent les cas d'utilisation de votre application.

Cependant, une classe instable est très sensible aux effets de bord, une anomalie ou tout changement dans l'une de ses dépendances pourrait directement se répercuter sur le fonctionnement de notre action.

Il devient essentiel de surveiller ces dépendances et de s'assurer qu'une classe instable ne soit pas dépendante d'une classe encore plus instable : l'instabilité en cascade génère de la fragilité.

Si une telle fragilité est identifiée, il devient alors intéressant de privilégier les abstractions plutôt que les implémentations concrètes en suivant le principe d'inversion de dépendance. Vous gagnerez ainsi en découplage en identifiant vos besoins dans des interfaces.

Une interface est, par nature, stable. Elle possède généralement moins de dépendances que les classes, ce qui réduit son efferent coupling tout en étant souvent réutilisée par plusieurs autres classes, augmentant ainsi son couplage afférent. Cette combinaison confère aux interfaces une très forte stabilité permettant de corriger les fragilités dans vos dépendances.

Ces interfaces possèdent également une puissance structurelle souvent négligée car elles permettent de décrire un besoin fonctionnel de haut niveau, relativement immuable dans le temps, en déclarant ce que votre action doit accomplir plutôt que comment l'accomplir.

Réfléchissez aux besoins réels de vos composants instables et assurez vous qu'ils ne dependent pas d'une implémentation quelconque qui, par nature, sera limitante et bloquante pour l’évolutivité de votre code.

Un composant instable doit donc surveiller ses dépendances et remplacer les fragilités par des abstractions afin de gagner en découplage et de limiter les effets de bord potentiels de toutes ses dépendances.

La stabilité dans les arbres de dépendances

Inévitablement, les composants de votre application communiquent entre eux, tout le travail de conception consiste justement à réfléchir à ces interactions : vont elles dans le bon sens ?

Bien qu'inévitable, nous pouvons cependant nous demander si cet arbre de dépendance repose sur des bases solides ou, au contraire, montre des signes de fragilités.

Imaginons l'application suivante, un controler, utilisant une action communiquant avec quelques autres composants : un service, un client http, etc ...

Rappel, une flèche montre le sens de la dépendance : qui a connaissance de qui

Il est facile dans cet exemple de calculer l'instabilité de ces quelques composants en se basant sur leur efferent et afferent coupling que nous avons vu precedemment, nous obtenons ainsi les métriques suivantes :

L'analyse de l'instabilité des composants montre une fragilité dans les dépendances, le composant Gateway est dépendant d'un composant Client davantage instable que lui même.

Cette faiblesse est dangereuse pour l'architecture de votre projet : Gateway devient sensible aux effets de bord que le composant Client pourrait introduire en cassant le flux de dépendance : un composant stable ne doit pas dépendre d'un composant instable.

Comment remédier à cette fragilité ? Il devient utile d'ajouter une interface afin d'inverser la dépendance et de découpler le composant Gateway du composant Client.

Grâce à cette interface, nous reprenons facilement la main sur ce flux de dépendance et Gateway n'est plus dépendant d'un composant moins stable : les bases de votre architecture n'en seront que plus stables et plus évolutives.

Cette interface peut paraitre comme une solution magique, mais qu'en est il réellement ? Cette dernière laisse l'opportunité au composant Gateway de définir ses propres besoins fonctionnels de haut niveau que le composant Client devra implémenter.

Ces besoins fonctionnels sont relativement immuables dans le temps et apporteront une certaine solidité : quelle que soit la solution implémentée, le besoin fonctionnel reste le même.

Logique métier et détails d'implémentation évoluent à des rythmes différents

La Gateway est désormais moins affectée par les effets de bord causés par des changements dans ses dépendances car l'implémentation de ses besoins est un détail délégué au composant Client

Désormais, toutes les dépendances entre composants respectent le principe de décroissance de l'instabilité : les composants les plus hauts s'appuient sur des composants de plus en plus stables, comme par exemple toute la chaîne de dépendance présente entre le controller et le helper :

Comment mesurer la stabilité

Mesurer le couplage et l'instabilité d'une application est une tâche complexe et chronophage, d'autant que nos applications comptent souvent plusieurs centaines de composants.

L'organisation par défaut plébiscitée par nos frameworks complique encore davantage cette analyse, la structure par défaut d'une application Laravel se limite principalement à un découpage technique des préoccupations, sans constituer une véritable architecture à part entière.

L'architecture d'une application s'intéresse avant tout à l'interaction entre les composants (qui communique avec qui), cette réflexion est totalement absente d'un découpage technique des responsabilités et conduira à la création d'une application Big Ball of Mud.

L'exercice sera plus accessible dans une architecture en couches (ou Layers) comme l'hexagonale ou la Clean Architecture car vos composants auront déjà subi un découpage plus strict de leurs responsabilités.

Des outils existent pour simplifier et automatiser le calcul de la stabilité tels que PHPMetrics ou, plus récemment, php-class-dependencies-analyzer.

Pour autant, l'utilisation d'un outil n'est pas forcément primordiale : cet exercice peut avant tout être mental.

En gardant ces notions en tête, vous saurez rapidement identifier les classes stables et les composants instables dans vos applications, les premières sont généralement fortement réutilisées tandis que les seconds contiennent de nombreuses dépendances.

Vous pourrez ainsi suivre le principe SDP en vérifiant la stabilité de vos composants, la solidité de leurs dépendances et ainsi vous assurer de la qualité de votre architecture.

A lire

Autres articles de la même catégorie