Retour sur la lecture du livre Balancing Coupling in Software Design
Temps de lecture : environ 10 min.
Cet article est un partage de connaissances sur le couplage suite à ma lecture du livre de Vlad Khononov, Balancing coupling in software design. C’est donc ma compréhension, une sélection de ce que j’ai particulièrement apprécié. Le livre étant très riche, je ne fais ici que survoler ce qu’il traite du sujet.
Tout d’abord, mon impression globale sur le livre : c’est un des meilleurs livres technique que j’ai lu. Contrairement à son autre livre que j’ai (re)lu récemment, Learning Domain Driven Design, j’ai trouvé que ce livre allait au fond du sujet. Il ne m’a pas laissé sur ma faim, loin de là. J’ai adoré.
Couplage
Alors de quoi parle-t-on quand on dit “couplage” ?
L’auteur définit le couplage comme étant la connexion entre deux composants, cette connexion ayant deux composantes :
- la connaissance qui est partagée, qu’il nomme la force d’intégration
- le cycle de vie partagé, qu’il nomme la distance.
Kent Beck note qu’il définit le couplage différemment dans la préface du livre :
Vlad uses “integration strength” to mean what I mean by “coupling”, the relationship between elements where changing one in a particular way requires changing the other. He uses “coupling” to mean a more general connection between elements, at runtime or compile time. It’s not a huge deal but it’s important for me to say.
Vlad utilise “force d’intégration” pour désigner ce que j’entends par “couplage”, c’est-à-dire la relation entre les éléments où la modification de l’un d’eux d’une manière particulière nécessite la modification de l’autre. Il utilise “couplage” pour désigner une connexion plus générale entre les éléments, au moment de l’exécution ou de la compilation. Ce n’est pas très grave, mais il est important que je le dise.
J’utiliserai les mots de Khnonov dans la suite de cet article.
Une dernière précision de vocabulaire avant de se lancer, la notion de flux. Il y a un sens dans le couplage : un module en amont, upstream, fournit de la connaissance avec un module en aval, downstream qui consomme la connaissance, qui est dépendant.
Force d’intégration
Commençons donc par décrire la force d’intégration. Elle s’applique à tout niveau de module. Un module pouvant être une ligne de code, une fonction, une classe, un microservice. La force d’intégration représente la connaissance que partagent deux modules, et donc une dépendance entre eux. Il y a 4 niveaux, du plus faible au plus fort :
- le couplage de contrat
- le couplage de modèle
- le couplage fonctionnel
- le couplage intrusif
Couplage de contrat et de modèle
On a un couplage de contrat quand un module en aval est lié à un autre module en amont par un modèle d’intégration qui permet ainsi d’éviter d’exposer un modèle interne / d’implémentation. Par exemple au niveau d’une API REST, il est fortement conseillé de retourner un modèle qui est une vue de notre modèle métier spécifique pour l’API. Cela nous permet notamment d’avoir des différences dans les deux modèles qui servent des besoins différents. Je peux par exemple avoir des noms de champs différents, des champs présents dans un des modèles uniquement.
Dans le couplage de modèle le module consommateur connaît le modèle interne du module qu’il consomme. La connaissance partagée est donc plus forte. Et cela peut-être un choix normal dans certains cas. Le plus important ici est que cela doit être fait en toute connaissance de cause.
Dans ces 2 niveaux de couplage, de contrat et de modèle, on rencontre les partages de connaissance suivants (ordonnés du plus faible au plus fort) :
- par le nom. Ex : je dois connaître le nom de la méthode que j’appelle.
- par le type. Ex : je dois connaître les types utilisés, de mes paramètres d’entrée, de sortie.
- par le sens. Ex : si je mets le chiffre 1 dans le code pour représenter un état, au lieu de mettre une valeur d’énumération.
- par l’algorithme. Ex : si dans un module j’encode une chaîne en base 64, je dois le savoir dans le module appelant pour décoder la chaîne et appliquer le bon processus de décodage.
- par la position. Ex : quand on doit connaître l’ordre des valeurs d’un tableau retourné par une fonction.
Cette classification est celle de la connascence statique, qui est bien détaillée dans le livre (voici une ressource en ligne très intéressante pour approfondir). Ici elle est dite statique car l’impact est au niveau du code source, visible à la compilation.
Le partage de connaissance à ces 2 niveaux est limité à du partage de données.
Couplage fonctionnel
En revanche, pour le couplage fonctionnel, on partage aussi du comportement. On partage de la connaissance fonctionnelle (du moins fort au plus fort) :
- si on doit connaître des informations quant à l’exécution du module Par exemple quand on doit connaître l’ordre d’appel de méthodes d’une classe. Ou quand on doit respecter un délai entre deux appels de méthodes.
- si on a une dépendance transactionnelle entre les modules. Si je change un valeur, je dois absolument en changer une autre dans un autre module. L’opération devant être atomique. Soit les 2 modifications réussissent, soit on annule les deux.
- si on a de la duplication d’une même fonctionnalité.
On parle ici de connascence dynamique, le couplage pouvant être découvert à l’exécution. Une analyse statique ne nous permettra pas de le détecter.
Pour repérer ce type de couplage, on imagine un changement dans une fonctionnalité, et on regarde les impacts que cela aurait.
Couplage intrusif
Enfin, le dernier niveau de connaissance partagée, le pire, et, hélas, pas le moins fréquent : le couplage intrusif. Un module connaît les détails d’implémentation d’un module en amont, ou tape dans ses données, sans son accord. Le notion d’accord faisant toute la différence.
C’est par exemple souvent le cas quand on utilise l’api reflection
en Java.
Pas toujours, car par exemple la lib Jackson utilise cette api, c’est son fonctionnement normal, et donc on l’accepte en connaissance de cause.
Il y a donc accord. Mais si on l’utilise sur un module qui ne donne pas son accord, alors là c’est très problématique.
Si un changement a lieu dans le module upstream, le module downstream, intrusif, peut casser.
C’est aussi le cas quand un service vient se servir dans la base de données du service voisin. Si la structure change, ça casse.
Distance
L’autre composante à côté de la force d’intégration est la distance.
Unsplash/Forest Simon
La distance peut être organisationnelle : si deux modules sont gérés par deux équipes, cela n’a pas les mêmes conséquences que si ces deux modules sont gérés par une seule équipe. Avec deux équipes différentes se posent notamment des problèmes de communication qui n’existeront pas si on a une seule équipe. Elle peut être aussi physique : est-ce que ce sont des modules qui sont déployés dans une même unité de déploiement, ou séparément ? Plus les modules sont éloignés, distants, plus cela va augmenter le coût du changement en cas de connaissance partagée. Donc plus on a des modules qui changent en même temps, pour les mêmes raisons, plus ils doivent être proches. On a alors de la cohésion.
Volatilité
Une dernière notion importante, la volatilité. Elle ne décrit pas la connexion en elle-même entre les modules. Mais le rythme de changement. Est-ce que le module en amont change souvent ? Car un module qui ne change pas, n’aura pas d’impact sur les modules en aval. Cette notion sera utile dans la mesure du couplage.
Coût du couplage
Mais pourquoi s’intéresser au couplage, le mesurer ? Car il a un coût. Plus on a de distance entre des composants avec une forte force d’intégration, plus ce coût sera élevé. La complexité accidentelle, non nécessaire au bon fonctionnement du système, est croissante quand on a du couplage mal conçu.
Dependency is the key problem in software development at scale. — Ian Cooper
Plus on a de code, plus gérer la distribution de la connaissance est critique pour l’adaptabilité du système.
Kent Beck le dit aussi dans Tidy First ? : le coût d’un logiciel est environ égal au coût du changement, ce qui équivaut au coût des gros changements, qui sont contraints par le couplage.
Ce qu’il résume de la sorte : cost(software) ~= cost(change) ~= cost(big changes) ~= cost(coupling)
.
Il synthétise cela en : cost(software) ~= cost(coupling) + cost(decoupling)
.
Il faut des travaux (refactoring) pour casser le couplage si on veut faire évoluer le logiciel.
C’est le changement qui nous fait apparaître cela. J’imagine que cette situation vous parle : on vous demande une modification, vous annoncez 3 jours et on vous regarde avec étonnement. “Juste pour faire ça ?”. Et oui, un petit changement, mais qui a des impacts en cascade. Shotgun surgery. Et cela marche aussi pour des plus gros changements. Ce qui parfois peut finir par être catastrophique.
Le couplage peut donc tuer le logiciel, sa capacité d’adaptation. Que se passera-t-il si un changement réglementaire, avec une date butoir, impliquant une amende en cas de non respect, ne peut être réalisé ? Ou si on ne peut s’adapter à des changements technologiques, alors que la concurrence elle le peut ?
Mais le couplage est inévitable, c’est aussi ce qui apporte de la valeur. Sans couplage, rien ne fonctionne. C’est la colle du système.
Il est donc indispensable, mais son contrôle ne devrait pas nous échapper. Le mesurer peut donc nous permettre de le garder son contrôle.
Mesure du couplage
Il propose une mesure binaire, et une autre chiffrée. Je vous partage ici quelques équations du livre.
STABILITY = NOT(VOLATILITY AND STRENGTH)
:
La relation est stable si le module amont ne change pas, ou si la force d’intégration entre les deux modules est faible (couplage de contrat par exemple).
MODULARITY = STRENGTH XOR DISTANCE
:
Le système est modulaire si on a une faible force d’intégration et une grande distance, ou une forte force d’intégration et une distance faible.
Par exemple, deux microservices déployés indépendamment et sans connaissance partagée sont indépendants.
Impacter l’un n’implique pas d’impacter l’autre.
Inversement, si deux services sont très liés, par exemple par une transaction, mais qu’il réside dans le même module, l’impact du changement sera maîtrisé.
Tout comme la complexité :
COMPLEXITY = NOT(MODULARITY)
= NOT(STRENGTH XOR DISTANCE)
On a de la complexité dite locale si on a pas de force d’intégration mais des modules proches. Cela implique une charge cognitive inutile. On a de la complexité dite globale si on a une forte force d’intégration et de la distance.
L’équilibre du système dépend de ces variables :
BALANCE = NOT (COMPLEXITY AND VOLATILITY)
= MODULARITY OR NOT VOLATILITY
= (STRENGTH XOR DISTANCE) OR NOT VOLATILITY
Plus des modules sont liés, moins il faut de distance entre eux. Même si cela est limité par la vitesse de changement du module upstream. S’il change peu, alors cela aura moins de conséquences.
Son échelle de mesure numérique est aussi instructive, je vous la laisse découvrir dans le livre. Elle permet de mettre des valeurs sur les niveaux de couplage, et donc d’être plus fin dans l’analyse. Et donc de comparer des solutions.
Agir
Un système d’information est toujours en croissance, sauf quand il s’effondre, ou s’arrête. Il est donc vital de savoir adresser la complexité ajoutée accidentellement. Mesurer le couplage nous permet de mieux identifier les composants en souffrance, et comprendre ce qu’il faut faire pour limiter le coût du changement. Rapprocher ce qui est fortement couplé, éloigner ce qui ne l’est pas. On modularise. Une conception modulaire optimise la distribution de la connaissance dans un système. On introduit les bonnes abstractions, et une bonne séparation des responsabilités. Le livre de Khononov est d’ailleurs très complet à mon sens sur la notion d’abstraction et de module. Ces chapitres sont denses. Définir une bonne abstraction n’est pas trivial. Pour introduire les bonnes abstractions, il faut laisser du temps aux concepts pour bien les cerner, les voir. Abstraire trop tôt c’est souvent introduire de la complexité. Quelle frontière de connaissance embarquer ? Quelle interface exposer ? Il faut trouver le bon équilibre entre l’utilité de l’interface et sa généricité. Limiter la complexité exposée aux clients. Imposer des contraintes, elles sont indispensables pour réduire la complexité. Par exemple une méthode d’un repository ne permet pas à ses clients de l’appeler avec du SQL, ce serait déporter des responsabilités, de la complexité au mauvais endroit. Son interface doit déterminer ce qui est autorisé.
Pour résoudre les problèmes de complexité, pour mieux modulariser et abstraire il faut s’appuyer sur différentes pratiques. On gagnera à s’améliorer en refactoring, en architecture logicielle (et notamment en s’intéressant à l’architecture hexagonale). Mais aussi en se penchant sur la pensée systémique. Khononov commence d’ailleurs par décrire le logiciel comme étant un système, lui-même composé de sous-systèmes. Le livre étant déjà très dense, il n’aborde pas en détail ces sujets. Il faudra se tourner vers d’autres livres. Je reviendrai d’ailleurs vous parler bientôt de Learning System Thinking de Diana Montalion, un livre tout aussi passionnant.
Un classique
C’est donc une lecture riche, profonde, exigeante, mais qui m’a fait avancer. Le genre de livre qui relie des concepts que je connaissais depuis un certain temps, mais dont les contours étaient encore flous. Un livre éclairant.
Je le conseille à tout type de profil, junior ou expérimenté.
Un futur classique ?