Read in English · Leer en español · Auf Deutsch lesen
Section 3.1 sur 3.8 · Intermédiaire · 22 min de lecture
Modularité : diviser un système en parties
Leçon vidéo
5 min · narré en anglais, avec sous-titresÉcouter seulement (audio)
Lire la transcription
Tout grand système est divisé en parties. Dans cette leçon, vous allez découvrir ce qu’est un module, pourquoi on divise les systèmes en modules, et la règle la plus importante pour décider où passent les frontières.
Pensez à un grand hôpital. Ce n’est pas une immense salle où tout le monde fait tout. Il est divisé en services, comme la radiologie et la pharmacie, chacun avec son propre personnel. Les autres s’adressent à chaque service par un accueil, avec des formulaires de demande clairs. Ainsi, si la pharmacie change son système de gestion des stocks, les autres services ne s’en aperçoivent pas, car ils remplissent toujours le même formulaire. Le logiciel est divisé en modules de la même façon. Quand c’est bien fait, on peut comprendre un module sans lire le reste, les équipes peuvent travailler en parallèle, et un changement reste en un seul endroit. Mais seulement si les murs sont aux bons endroits.
Un module est une partie d’un système qui a une responsabilité claire et une manière définie dont les autres parties peuvent l’utiliser. Cela ne dit rien de la taille : un module peut être une fonction, une classe, un paquet, un composant ou un service entier. Son interface est ce que les autres doivent savoir pour l’utiliser. Son implémentation est tout le reste. Une bonne modularité garde l’interface petite et stable, pour que l’implémentation puisse changer librement.
En 1972, David Parnas a montré à quel point le choix des frontières compte, à l’aide d’un petit programme qui construit un index de mots clés dans leur contexte, pour qu’un lecteur puisse trouver un titre sous n’importe lequel de ses mots. La première conception suit l’organigramme, avec un module pour chaque étape de traitement : entrée, décalage circulaire, tri alphabétique et sortie. C’est une conception parfaitement raisonnable. Mais chaque module connaît les formats de données partagés en mémoire. Si bien qu’un changement dans la façon de stocker les lignes les touche tous les quatre. La seconde conception paraît plus étrange. Ses modules ne correspondent pas aux étapes. À la place, chacun cache une seule décision de conception, comme le format d’entrée ou le moment du tri, derrière des fonctions d’accès. Désormais, un changement dans la façon de stocker les lignes reste à l’intérieur d’un seul module, le stockage des lignes. Les autres se contentent d’appeler ses fonctions.
Parnas en a conclu qu’il est presque toujours mauvais de partir d’un organigramme. Il faut plutôt lister les décisions de conception difficiles, ou susceptibles de changer, et confier à chaque module l’une d’elles à cacher. Il a appelé cela le masquage de l’information. Un organigramme décrit le temps : d’abord ceci, ensuite cela. Mais les décisions de conception traversent le temps. La façon dont une commande est représentée compte quand elle est reçue, tarifée, payée et enregistrée. Si un seul module possède cette décision, elle peut changer en un seul endroit.
Ensuite, une distinction importante. Un module logique est une unité de responsabilité dans la conception, comme Catalogue ou Paiements. Un module physique est une unité que l’on construit et que l’on déploie, comme un paquet ou une image de conteneur. Les mêmes modules logiques peuvent être déployés en une seule unité, en s’appelant au sein du même processus : c’est rapide, avec une seule mise en production. C’est un monolithe modulaire. Ou bien chacun peut être un service à part, appelé par le réseau. Chacun peut alors être mis en production séparément, mais vous le payez en latence, en défaillances partielles et en versionnage. Découper en services un système mal divisé ne cache rien. Vous obtenez simplement des services enchevêtrés, ce que l’on appelle parfois un monolithe distribué. Commencez par bien définir les frontières logiques, et seulement ensuite décidez lesquelles méritent de devenir physiques.
Mettons cela à l’épreuve sur le processus de commande d’une boutique en ligne. Une conception comporte six étapes d’organigramme qui partagent un même enregistrement de commande. L’autre comporte six modules, chacun cachant une décision. Nous comptons les modules que toucheraient cinq changements attendus. Un second prestataire de paiement touche deux étapes de l’organigramme, mais seulement le module de passerelle de paiement. Une modification des règles de TVA touche trois étapes, mais seulement le module Taxes. Découper l’adresse de livraison en champs structurés touche les six étapes, car elles lisent toutes l’enregistrement partagé. Dans l’autre conception, seul le module Commande change. Certains changements coûtent autant dans les deux cas. Les confirmations par SMS touchent un module dans les deux, et une fonctionnalité réellement nouvelle, un message cadeau, en touche trois dans les deux. Au total, quinze contre sept. Lister les changements probables et compter les modules que chacun touche est un moyen rapide de comparer des conceptions lors d’une revue.
Quelle taille doit avoir un module ? Des modules trop grands mélangent des raisons de changer sans rapport entre elles, et de nombreuses personnes modifient les mêmes fichiers. Des modules trop petits multiplient les interfaces, si bien qu’une petite fonctionnalité exige des changements à de nombreux endroits. À travers un réseau, chaque interface coûte aussi de la latence, du versionnage et une nouvelle façon d’échouer : les services doivent donc être bien plus grossiers que les classes. Et une frontière qui n’existe que sur un schéma ne survivra pas à une échéance. Les professionnels la font respecter avec des règles de visibilité, des systèmes de modules et des vérifications automatisées des dépendances qui font échouer le build.
La section complète présente en détail l’exemple de Parnas et la façon dont chaque langage fait respecter les frontières. Ensuite, dans la section trois point deux, nous verrons le couplage et la cohésion. À très vite.
Ce qu’il faut savoir
- Un module est une unité de décomposition dotée d’une responsabilité et d’une interface : une fonction, une classe, un paquet, un composant ou un service, selon l’échelle à laquelle vous travaillez.
- La modularité est payante grâce à la compréhension, au travail en parallèle des équipes, à l’isolation des changements, à la réutilisation et aux tests, mais seulement si les frontières sont tracées aux bons endroits.
- David Parnas (1972) a montré avec l’exemple KWIC que décomposer selon les étapes de traitement disperse chaque décision de conception dans de nombreux modules, et a proposé à la place que chaque module cache une décision difficile ou susceptible de changer.
- Les modules logiques (unités de responsabilité dans la conception) se distinguent des modules physiques (unités de construction et de déploiement) ; une même conception logique peut être déployée en une seule unité ou en de nombreux services.
- La granularité est un compromis : des modules trop grands mélangent des changements sans rapport entre eux, tandis que des modules trop petits multiplient les interfaces, la coordination et, à travers un réseau, la latence et les modes de défaillance.
- Les frontières qui ne sont pas imposées s’érodent : les professionnels utilisent donc la visibilité du langage, des systèmes de modules comme le Java Platform Module System, les modules des outils de build et des vérifications automatisées des dépendances.
Ce qu’est un module
Un module est une partie d’un système qui a une responsabilité claire et une manière définie dont les autres parties peuvent l’utiliser. Cette définition est volontairement indépendante de la taille. Dans la section 0.3, un module était un fichier Python ; dans ce chapitre, le mot couvre toute unité de décomposition, c’est-à-dire l’action de diviser un système en parties : une fonction, une classe, un paquet, une bibliothèque, un composant à l’intérieur d’une application, ou un service déployé séparément. Les mêmes questions se posent à toutes les échelles : de quoi cette partie est-elle responsable, qu’offre-t-elle aux autres, que garde-t-elle pour elle, et de quoi dépend-elle ?
Tout module a deux faces. Son interface est ce que les autres modules doivent savoir pour l’utiliser : noms et paramètres des fonctions, une API HTTP, les événements qu’il publie, mais aussi les promesses informelles, comme sa rapidité de réponse et les erreurs qu’il peut renvoyer. Son implémentation est tout le reste : le code, les structures de données, les tables de la base de données, les bibliothèques tierces qu’il se trouve utiliser. Une bonne modularité garde l’interface petite et stable, et laisse l’implémentation changer librement.
David Parnas, dont l’article de 1972 est le fondement de cette section, définissait un module comme une attribution de responsabilité plutôt que comme un morceau de texte de programme. C’est la façon la plus utile d’y penser quand on est architecte. Un module est la promesse qu’une partie du système, et en général une équipe, est responsable d’un ensemble de décisions. Que le code finisse dans un fichier ou dans quarante, dans un processus ou dans dix, est une question distincte, traitée plus bas à propos des modules logiques et physiques.
Pourquoi modulariser
Un système de quelques centaines de lignes peut être compris dans son ensemble. Un système de quelques millions de lignes ne le peut pas, et l’organisation qui le construit ne peut pas non plus travailler comme un seul groupe indivisé. La modularité est la propriété d’être divisé en modules qui peuvent être compris, modifiés et analysés en grande partie isolément. C’est l’une des cinq sous-caractéristiques de la maintenabilité dans ISO/IEC 25010 (section 2.6), et elle procure au moins cinq avantages distincts.
La compréhension. Un développeur peut étudier un module et ses interfaces sans lire le reste du système. Le travail en parallèle. Les équipes peuvent travailler sur des modules différents en même temps avec peu de besoin de coordination, parce qu’elles se sont mises d’accord à l’avance sur les interfaces. L’isolation des changements. Un changement portant sur une décision de conception, par exemple les règles fiscales applicables, est fait dans un seul module et ne se propage pas. La réutilisation. Un module bien délimité, comme un composant de validation d’adresses, peut être utilisé par plusieurs produits. Les tests. Un module doté d’une interface claire peut être testé isolément, ses dépendances étant remplacées par des doublures de test.
Parnas en a cité trois en 1972 : l’avantage managérial (un temps de développement raccourci, parce que des groupes distincts travaillent sur des modules distincts en communiquant peu), la flexibilité (des changements radicaux dans un module sans modifier les autres) et la compréhensibilité (étudier le système un module à la fois). Un demi-siècle plus tard, ce sont toujours les raisons. Mais remarquez qu’aucun de ces avantages ne découle automatiquement du découpage d’un système en morceaux. Un système peut être divisé en cinquante modules et ne procurer aucun de ces avantages si chaque changement en touche dix. Les avantages dépendent entièrement de l’endroit où les frontières sont tracées.
Parnas et l’exemple KWIC
Parnas a illustré son propos avec un petit programme appelé KWIC (key word in context, mot clé dans son contexte), qui produit un index de titres. Il lit des lignes de texte, génère chaque décalage circulaire de chaque ligne (en déplaçant à plusieurs reprises le premier mot à la fin), trie tous les décalages par ordre alphabétique et les imprime. La ligne Design of Software produit Design of Software, of Software Design et Software Design of, de sorte qu’un lecteur peut trouver le titre sous n’importe lequel de ses mots.
La première décomposition qu’il décrit est celle que produiraient la plupart des programmeurs en dessinant un organigramme : un module Entrée qui lit les lignes et les stocke en mémoire dans un format compacté, un module Décalage circulaire qui construit un index indiquant où commence chaque décalage, un module Tri alphabétique qui trie cet index, un module Sortie qui imprime le résultat, et un module de contrôle principal qui les appelle dans l’ordre. C’est une conception parfaitement raisonnable. Chaque module est petit et les interfaces entre eux sont les formats de données en mémoire.
La seconde décomposition paraît plus étrange. Elle comprend un module Stockage des lignes qui offre des fonctions comme donner le caractère de la ligne r, mot w, position c et cache la façon dont les lignes sont réellement stockées ; un module Entrée qui lit les lignes et les confie au Stockage des lignes ; un Décaleur circulaire qui présente les décalages comme s’ils étaient stockés, au moyen de fonctions d’accès similaires ; un Trieur qui offre donne-moi le i-ème décalage dans l’ordre sans dire quand ni comment il trie ; un module Sortie ; et un contrôle principal. Les modules ne correspondent plus aux étapes du traitement.
Parnas a ensuite listé les décisions discutables et susceptibles de changer : le format d’entrée ; le fait de garder toutes les lignes en mémoire, ce qui n’est pas réaliste pour de très grandes entrées ; le fait de compacter plusieurs caractères dans chaque mot machine ; le fait de stocker un index des décalages plutôt que les décalages eux-mêmes ; et le fait de tout trier une fois plutôt que de rechercher à la demande. Dans la première décomposition, la plupart de ces changements touchent plusieurs modules, car chaque module connaît les formats de données partagés. Dans la seconde, chaque changement reste confiné au seul module qui cache la décision correspondante. La seconde conception était aussi plus facile à développer en parallèle, parce que les équipes n’avaient à s’accorder que sur des noms de fonctions et des paramètres, et non sur des structures de tables détaillées, et plus facile à comprendre un module à la fois.
Les deux décompositions de KWIC selon Parnas
Le critère : décomposer selon les décisions susceptibles de changer
Parnas concluait qu’il est presque toujours incorrect
de commencer à décomposer un système à partir d’un organigramme. Il proposait plutôt de partir d’une liste de décisions de conception difficiles, ou de décisions de conception susceptibles de changer, et de concevoir chaque module pour qu’il cache l’une de ces décisions aux autres. Il a appelé ce principe le masquage de l’information (information hiding) ; la section 3.3 le traite en profondeur.
L’idée paraît simple, mais elle va à l’encontre de l’instinct de la plupart d’entre nous. Un organigramme décrit le temps : d’abord ceci, ensuite cela. Les décisions de conception traversent le temps. La décision sur la façon de représenter une commande sert quand la commande est reçue, tarifée, payée, enregistrée et confirmée. Si les modules suivent les étapes, cette décision unique est répartie entre toutes, et la changer impose de toutes les changer. Si un module possède la décision, toutes les étapes s’adressent à ce module, et la décision peut changer en un seul endroit.
Deux précisions comptent en pratique. D’abord, le critère porte sur les changements probables, ce qui signifie qu’un architecte doit faire une supposition éclairée sur l’avenir : ce qui a changé dans des systèmes similaires, ce que l’entreprise prévoit, quels fournisseurs et quelles réglementations sont instables. Les quatre questions de modifiabilité de la section 2.6 (qu’est-ce qui change, avec quelle probabilité, qui le change, à quel coût) sont exactement les données d’entrée dont il a besoin. Ensuite, Parnas distinguait une bonne décomposition d’une hiérarchie. Un système peut être proprement organisé en couches et pourtant mal décomposé, et inversement ; la section 3.4 traite séparément du sens des dépendances.
Modules logiques et modules physiques
Un module logique est une unité de responsabilité dans la conception : Catalogue, Commandes, Paiements. Un module physique est une unité que l’on construit, que l’on versionne ou que l’on déploie : un fichier JAR Java, un wheel Python, un paquet npm, une image de conteneur, une fonction serverless. La section 1.1 les a appelées structures de modules et structures d’allocation. Les deux sont liées, mais ce n’est pas la même chose, et les confondre est à l’origine de certaines des erreurs les plus coûteuses de l’architecture moderne.
Une même conception logique peut être projetée sur des modules physiques de façons très différentes. Tous les modules logiques peuvent vivre dans une seule unité déployable et s’appeler les uns les autres au sein du même processus : c’est un monolithe modulaire. Ou bien chacun peut être déployé comme un service à part, qui appelle les autres par le réseau : ce sont des microservices. Les frontières logiques, et les décisions que cache chaque module, peuvent être identiques dans les deux cas. Ce qui change, c’est le coût du franchissement d’une frontière (un appel de fonction prend des nanosecondes, un appel réseau des millisecondes, et il peut échouer), la possibilité de mettre en production et de faire passer à l’échelle des parties de façon indépendante, et le travail d’exploitation.
Une conception logique, deux conceptions physiques
Une erreur courante et coûteuse consiste à traiter la séparation physique comme si elle créait la modularité logique. Découper en services un système mal décomposé ne cache aucune décision ; cela transforme des appels internes entre modules enchevêtrés en appels réseau entre services enchevêtrés, ce que l’on appelle parfois un monolithe distribué. L’erreur inverse existe aussi : une équipe garde une seule unité déployable mais laisse ses modules internes s’imbriquer les uns dans les autres, si bien qu’elle ne pourra jamais les séparer par la suite. Le conseil pratique de nombreux praticiens est de commencer par bien définir les frontières logiques, de les faire respecter, et seulement ensuite de décider lesquelles méritent de devenir des frontières physiques.
La granularité et ses coûts
Quelle taille doit avoir un module ? La granularité est la taille des modules par rapport au système, et il n’existe pas de bonne réponse universelle, car les coûts tirent dans des directions opposées. Les modules plus grands ont moins d’interfaces à concevoir et à maintenir, mais chacun contient davantage de raisons de changer sans rapport entre elles, davantage de personnes qui le modifient en même temps, et davantage de choses à comprendre avant de faire un changement. Les modules plus petits sont chacun plus faciles à comprendre, mais le système a plus d’interfaces, plus de dépendances à gérer et plus d’endroits où une même fonctionnalité doit être assemblée morceau par morceau.
Le coût d’une interface dépend aussi du type de frontière. Entre deux classes d’un même programme, une interface supplémentaire coûte un peu de temps de lecture. Entre deux services, elle coûte un saut réseau, de la sérialisation, une politique de versionnage, de la supervision, un mode de défaillance et peut-être l’accord d’une seconde équipe pour chaque changement. C’est pourquoi la bonne granularité pour les services est généralement bien plus grossière que la bonne granularité pour les classes.
Symptômes d’une mauvaise granularité
| Trop grossière | Trop fine |
|---|---|
| Des fonctionnalités sans rapport modifient le même module ; de nombreuses personnes modifient les mêmes fichiers et entrent en conflit dans la gestion de versions. | Une seule petite fonctionnalité exige des changements coordonnés dans de nombreux modules ou services. |
| De longs temps de build et de test pour chaque changement ; personne ne comprend le module dans son ensemble. | De nombreuses interfaces triviales de simple transmission qui n’apportent aucune abstraction ; des chaînes d’appels qui traversent plusieurs modules. |
| Les équipes font la queue pour mettre en production, parce que tout est livré ensemble. | À travers un réseau : une latence élevée due à des appels trop bavards, des défaillances partielles et des transactions distribuées. |
Le nom du module est vague, comme core, common ou manager. | Chaque module est superficiel : son interface est presque aussi complexe que son implémentation (voir la section 3.3). |
Faire respecter les frontières des modules dans le code
Une frontière qui n’existe que sur un schéma ne survivra pas à l’épreuve d’une échéance. Tôt ou tard, quelqu’un importera une classe interne parce que c’est plus rapide, et dès qu’un raccourci existe, d’autres suivent ; la section 1.7 a appelé cela l’érosion. Les langages et les outils de build offrent plusieurs moyens de faire de la frontière voulue le chemin de moindre résistance, ou de faire échouer le build quand on la franchit.
Python repose sur des conventions : les noms qui commencent par un tiret bas sont considérés comme internes (PEP 8), et le fichier __init__.py d’un paquet peut ne réexporter que ses fonctions publiques. Rien n’empêche d’importer shop.payments._stripe_client : les équipes soucieuses de leurs frontières utilisent donc un vérificateur comme import-linter pour faire échouer le build quand un import interdit apparaît. Java offre la visibilité package-private (la visibilité par défaut quand aucun modificateur n’est indiqué) et, depuis Java 9 en 2017, le Java Platform Module System : un fichier module-info.java déclare de quels modules celui-ci a besoin (requires) et lesquels de ses paquets il exporte (exports) ; les paquets qui ne sont pas exportés ne peuvent pas être utilisés de l’extérieur, ni à la compilation ni à l’exécution. Go considère tout paquet situé sous un répertoire nommé internal comme importable uniquement depuis le code enraciné dans le parent de ce répertoire. C# dispose de internal, visible uniquement au sein d’un assembly ; Rust, de pub(crate).
# shop/payments/__init__.py (the public face of the package)
from shop.payments.api import charge, refund, PaymentResult
__all__ = ["charge", "refund", "PaymentResult"]
# shop/payments/_provider_client.py internal: leading underscore
# shop/payments/_retry.py internal
# shop/payments/api.py the only module callers should use
# Elsewhere in the shop:
from shop.payments import charge # fine
from shop.payments._retry import backoff # works, but breaks the boundary// module-info.java for the payments module
module shop.payments {
requires shop.orders.api; // what this module depends on
exports shop.payments.api; // the only package others can use
// shop.payments.provider and shop.payments.retry are not exported,
// so code in other modules cannot compile against them
}Moyens de faire respecter une frontière
| Mécanisme | Exemples | Force |
|---|---|---|
| Convention de nommage | Tiret bas initial en Python ; internal dans un nom d’espace de noms | Faible : repose sur la discipline et la revue de code |
| Visibilité du langage | Package-private en Java, internal en C#, pub(crate) en Rust, répertoires internal en Go | Forte au sein d’un même build ; vérifiée par le compilateur |
| Système de modules du langage | Java Platform Module System (requires, exports) ; le champ exports d’un package.json Node.js | Forte : un accès non déclaré échoue à la compilation ou au chargement |
| Modules des outils de build | Builds multimodules Maven ou Gradle ; règles de visibilité Bazel ; paquets séparés dans un monorepo | Forte : un module ne peut utiliser que ce qu’il déclare comme dépendance |
| Vérifications automatisées de l’architecture | import-linter, ArchUnit, dependency-cruiser (section 3.4) | Forte si elles sont exécutées dans le pipeline de build comme fonction d’aptitude |
| Frontière de processus et de réseau | Services séparés avec leurs propres stockages de données, accessibles uniquement par une API | La plus forte, et la plus coûteuse à franchir et à modifier |
Exemple commenté
Exemple commenté : décomposer une boutique en ligne par organigramme et par changement probable
Le processus de commande d’une boutique en ligne est d’abord conçu comme un organigramme : Recevoir la commande, Valider le panier, Calculer le prix (promotions et TVA), Encaisser le paiement, Enregistrer la commande, Envoyer la confirmation. Les six étapes lisent et écrivent toutes un même enregistrement de commande partagé, un dictionnaire dont chaque étape connaît les clés. La conception alternative compte six modules, chacun cachant une décision : Commande (comment une commande est représentée et stockée), Tarification (règles de promotion), Taxes (règles de TVA), Passerelle de paiement (quel prestataire et comment l’appeler), Notifications (canaux et modèles de messages), et un Flux de validation qui ne fait qu’enchaîner les appels, comme le contrôle principal de Parnas. Cinq changements que l’entreprise prévoit pour l’année à venir sont comptés pour les deux conceptions.
| Changement 1 : ajouter un second prestataire de paiement | Organigramme : Encaisser le paiement, plus Enregistrer la commande, qui stocke le format de référence de transaction du prestataire = 2 modules. Par changement : Passerelle de paiement = 1. |
|---|---|
| Changement 2 : les règles de TVA d’une catégorie de produits changent | Organigramme : Calculer le prix, Enregistrer la commande (colonnes de TVA) et Envoyer la confirmation (détail de la TVA) = 3. Par changement : Taxes = 1. |
| Changement 3 : découper l’adresse de livraison en champs structurés | Organigramme : chaque étape lit l’enregistrement de commande partagé = 6. Par changement : Commande = 1 (les autres appellent ses méthodes). |
| Changement 4 : ajouter des confirmations par SMS | Organigramme : Envoyer la confirmation = 1. Par changement : Notifications = 1. |
| Changement 5 : nouvelle fonctionnalité, un message cadeau sur les commandes | Organigramme : Recevoir la commande, Enregistrer la commande, Envoyer la confirmation = 3. Par changement : Flux de validation, Commande, Notifications = 3. |
| Total des modules touchés | Organigramme : 2 + 3 + 6 + 1 + 3 = 15. Par changement : 1 + 1 + 1 + 1 + 3 = 7. |
Les changements portant sur des décisions existantes (prestataire, règles fiscales, représentation) restent à l’intérieur d’un seul module dans la conception fondée sur le changement ; c’est exactement ce que Parnas avait prédit.
Certains changements coûtent autant dans les deux conceptions (SMS), et une fonctionnalité réellement nouvelle (message cadeau) traverse plusieurs modules dans les deux. Le masquage de l’information protège les décisions que vous avez anticipées, pas toutes les fonctionnalités possibles.
Ce décompte est un moyen rapide et reproductible de comparer des décompositions candidates lors d’une revue de conception : listez d’abord les changements probables, puis comptez les modules que chacun touche.
Erreurs courantes
Décomposer uniquement par couche technique. Des dossiers appelés controllers, services et repositories sont des modules d’une certaine sorte, mais un changement dans le fonctionnement de la TVA touchera les trois. Les couches sont utiles (section 3.4), mais une décomposition qui n’a que des couches est un organigramme déguisé. La plupart des systèmes matures combinent un découpage par capacité métier et une organisation en couches à l’intérieur de chaque partie.
Tout partager. Un module appelé common ou utils que tous les autres modules importent devient l’endroit où s’accumulent des décisions sans rapport entre elles, et tout changement qui le touche se propage partout. Des frontières sans contrôle. Les schémas montrent des modules propres alors que le code contient des centaines d’imports croisés. Des modules définis par l’organigramme d’une réorganisation passée plutôt que par les décisions que le système doit cacher ; la section 3.7 explique pourquoi les organisations et les frontières des modules s’influencent mutuellement. La distribution prématurée. Transformer chaque module logique en service avant que les frontières aient prouvé leur stabilité rend coûteuses les corrections ultérieures de frontières.
Liste de contrôle : examiner une proposition de décomposition
- Nommez la décision que cache chaque moduleSi vous ne pouvez pas dire en une phrase ce qu’un module garde secret, il s’agit probablement d’une étape de traitement ou d’un fourre-tout.
- Listez les changements probablesNotez cinq à dix changements que l’activité ou la technologie risquent d’apporter, et comptez les modules que chacun toucherait.
- Vérifiez la taille de l’interfaceL’interface doit être bien plus petite que l’implémentation. De grandes interfaces avec peu de comportement derrière elles suggèrent un mauvais découpage.
- Séparez le logique du physiqueMettez-vous d’abord d’accord sur les modules logiques ; décidez séparément lesquels méritent leur propre unité déployable, et pourquoi.
- Décidez comment la frontière sera imposéeChoisissez un mécanisme de visibilité ou une vérification automatisée, et ajoutez-le au build avant que le premier raccourci n’apparaisse.
- Alignez les frontières sur la responsabilité des équipesChaque module devrait avoir une seule équipe propriétaire ; des modules partagés par plusieurs équipes pour l’essentiel de leur travail sont un signal d’alerte.
Comment les professionnels appliquent la modularité
Les architectes expérimentés considèrent la structure des modules comme l’artefact de conception le plus important pour la maintenabilité, et ils y reviennent régulièrement. Au début de la vie d’un produit, quand le domaine est mal compris, ils préfèrent des modules moins nombreux et plus grands, avec de solides frontières internes, parce que fusionner et scinder des modules au sein d’une même base de code coûte peu. À mesure que le domaine se stabilise et que les changements probables deviennent clairs, ils affinent les frontières et ce n’est qu’alors qu’ils se demandent lesquelles devraient être déployées séparément. Les techniques de la conception pilotée par le domaine, comme les contextes délimités (bounded contexts), aident à trouver des frontières qui suivent le langage du métier.
Ils mesurent aussi. Compter les modules touchés par les changements récents, ce que l’historique de la gestion de versions rend facile, montre si les frontières fonctionnent ; la section 3.8 présente le couplage de changement et les hotspots (points chauds). La section suivante, la section 3.2, donne le vocabulaire pour décrire à quoi ressemblent de bonnes et de mauvaises frontières : le couplage entre modules et la cohésion à l’intérieur de chacun.
Vérifiez votre compréhension
Pourquoi Parnas appelait-il un module une « attribution de responsabilité » plutôt qu’un sous-programme ?
Parce que ce qui compte, ce sont les décisions de conception dont un module est responsable et qu’il cache, et non la façon dont son code est empaqueté ; une responsabilité peut s’étendre sur plusieurs routines, et les routines n’ont pas à correspondre aux étapes de traitement.
Dans l’exemple KWIC, pourquoi le changement de la façon de stocker les lignes touchait-il plusieurs modules dans la première décomposition ?
Tous les modules partageaient directement les formats de données en mémoire : la décision de stockage était donc connue de chaque module au lieu d’être cachée dans un seul.
Quelle est la différence entre un module logique et un module physique ?
Un module logique est une unité de responsabilité dans la conception ; un module physique est une unité que l’on construit ou que l’on déploie, comme un JAR, un paquet, un conteneur ou un service. Une même conception logique peut être empaquetée physiquement de nombreuses façons.
Citez deux signes indiquant que des services sont trop fins.
De petites fonctionnalités exigent des changements coordonnés dans plusieurs services, et les requêtes passent par des chaînes bavardes d’appels réseau, avec une latence élevée et des défaillances partielles.
Sources et lectures complémentaires
Une question sur cette section ? ArchitectBot, l’assistant d’étude IA du cours, répond d’abord à partir du cours.
Demander : « Comment décider où tracer les frontières des modules dans un monolithe existant, avant même de penser à le découper en services ? »