Conception du codebase
Concevoir des modules profonds : beaucoup de comportement derrière une petite interface, placée à une couture propre, testable via cette interface. Utiliser ce langage et ces principes partout où du code est conçu ou restructuré. L'objectif est le levier pour les appelants, la localité pour les mainteneurs, et la testabilité pour tout le monde.
Glossaire
Utiliser ces termes exactement : ne pas substituer « composant », « service », « API » ou « frontière ». Un langage cohérent est tout l'intérêt.
Module : tout ce qui a une interface et une implémentation. Volontairement indépendant de l'échelle : une fonction, une classe, un paquetage ou une tranche traversant plusieurs niveaux. À éviter : unité, composant, service.
Interface : tout ce qu'un appelant doit connaître pour utiliser correctement le module : la signature de type, mais aussi les invariants, les contraintes d'ordre, les modes d'erreur, la configuration requise et les caractéristiques de performance. À éviter : API, signature (trop étroites, elles ne désignent que la surface typée).
Implémentation : ce qui se trouve à l'intérieur d'un module, son corps de code. Distinct de Adaptateur : une chose peut être un petit adaptateur avec une grande implémentation (un dépôt Postgres) ou un grand adaptateur avec une petite implémentation (un faux en mémoire). Utiliser « adaptateur » lorsque la couture est le sujet ; « implémentation » sinon.
Profondeur : levier à l'interface. La quantité de comportement qu'un appelant (ou un test) peut exercer par unité d'interface qu'il doit apprendre. Un module est profond lorsqu'une grande quantité de comportement se trouve derrière une petite interface, peu profond lorsque l'interface est presque aussi complexe que l'implémentation.
Couture (Michael Feathers) : un endroit où l'on peut modifier le comportement sans éditer à cet endroit ; l'emplacement où vit l'interface d'un module. Où placer la couture est une décision de conception en soi, distincte de ce qui se trouve derrière elle. À éviter : frontière (surchargée par le contexte borné du DDD).
Adaptateur : une chose concrète qui satisfait une interface à une couture. Décrit le rôle (quel emplacement il remplit), pas la substance (ce qu'il contient).
Leviers : ce que les appelants obtiennent de la profondeur. Plus de capacité par unité d'interface qu'ils apprennent. Une implémentation paie sur N sites d'appel et M tests.
Localité : ce que les mainteneurs obtiennent de la profondeur. Les changements, les bugs, les connaissances et la vérification se concentrent en un seul endroit plutôt que de se disperser chez les appelants. Corriger une fois, corrigé partout.
Profond vs peu profond
Module profond = petite interface + beaucoup d'implémentation :
┌─────────────────────┐
│ Petite Interface │ ← Peu de méthodes, paramètres simples
├─────────────────────┤
│ │
│ Implémentation │ ← Logique complexe cachée
│ Profonde │
│ │
└─────────────────────┘
Module peu profond = grande interface + peu d'implémentation (à éviter) :
┌─────────────────────────────────┐
│ Grande Interface │ ← Beaucoup de méthodes, paramètres complexes
├─────────────────────────────────┤
│ Implémentation fine │ ← Juste du passage
└─────────────────────────────────┘
Lors de la conception d'une interface, demandez-vous :
- Puis-je réduire le nombre de méthodes ?
- Puis-je simplifier les paramètres ?
- Puis-je cacher plus de complexité à l'intérieur ?
Principes
- La profondeur est une propriété de l'interface, pas de l'implémentation. Un module profond peut être composé intérieurement de petites parties mockables et échangeables ; elles ne font simplement pas partie de l'interface. Un module peut avoir des coutures internes (privées à son implémentation, utilisées par ses propres tests) ainsi que la couture externe à son interface.
- Le test de suppression. Imaginez supprimer le module. Si la complexité disparaît, c'était un passe-plat. Si la complexité réapparaît chez N appelants, il méritait sa place.
- L'interface est la surface de test. Les appelants et les tests traversent la même couture. Si vous voulez tester au-delà de l'interface, le module a probablement la mauvaise forme.
- Un adaptateur signifie une couture hypothétique. Deux adaptateurs signifient une couture réelle. N'introduisez pas une couture à moins que quelque chose ne varie réellement à travers elle.
Conception pour la testabilité
Les bonnes interfaces rendent les tests naturels :
-
Accepter les dépendances, ne pas les créer.
// Testable function processOrder(order, paymentGateway) {} // Difficile à tester function processOrder(order) { const gateway = new StripeGateway(); } -
Retourner des résultats, ne pas produire d'effets de bord.
// Testable function calculateDiscount(cart): Discount {} // Difficile à tester function applyDiscount(cart): void { cart.total -= discount; } -
Petite surface d'interface. Moins de méthodes = moins de tests nécessaires. Moins de paramètres = configuration de test plus simple.
Relations
- Un Module a exactement une Interface (la surface qu'il présente aux appelants et aux tests).
- La Profondeur est une propriété d'un Module, mesurée par rapport à son Interface.
- Une Couture est l'endroit où vit l'Interface d'un Module.
- Un Adaptateur se trouve à une Couture et satisfait l'Interface.
- La Profondeur produit du Levier pour les appelants et de la Localité pour les mainteneurs.
Cadres rejetés
- Profondeur comme rapport entre lignes d'implémentation et lignes d'interface (Ousterhout) : récompense le gonflement de l'implémentation. Nous utilisons plutôt la profondeur comme levier.
- « Interface » comme le mot-clé TypeScript
interfaceou les méthodes publiques d'une classe : trop étroit : l'interface inclut ici tout fait qu'un appelant doit connaître. - « Frontière » : surchargé par le contexte borné du DDD. Dites couture ou interface.
Aller plus loin
- Approfondir un cluster étant donné ses dépendances, voir DEEPENING.md : catégories de dépendances, discipline de couture, et remplacer plutôt que superposer lors des tests.
- Explorer des interfaces alternatives, voir DESIGN-IT-TWICE.md : lancer des sous-agents parallèles pour concevoir l'interface de plusieurs façons radicalement différentes, puis comparer sur la profondeur, la localité et le placement de la couture.