Conception de codebase
Concevez des modules profonds : beaucoup de comportement derrière une petite interface, placée à une jointure propre, testable via cette interface. Utilisez ce langage et ces principes partout où du code est conçu ou restructuré. L'objectif est l'effet de levier pour les appelants, la localité pour les mainteneurs et la testabilité pour tout le monde.
Glossaire
Utilisez ces termes exactement : ne substituez pas « composant », « service », « API » ou « frontière ». Un langage cohérent est tout l'intérêt.
Module : tout ce qui possède une interface et une implémentation. Délibérément indépendant de l'échelle : une fonction, une classe, un package ou une tranche couvrant plusieurs niveaux. Évitez : 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. Évitez : API, signature (trop étroits, ils ne désignent que la surface au niveau des types).
Implémentation : ce qui se trouve à l'intérieur d'un module, son corps de code. Distincte d'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). Utilisez « adaptateur » lorsque la jointure est le sujet ; « implémentation » sinon.
Profondeur : effet de 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.
Jointure (Michael Feathers) : un endroit où vous pouvez modifier le comportement sans éditer à cet endroit ; l'emplacement où vit l'interface d'un module. Où placer la jointure est une décision de conception en soi, distincte de ce qui se trouve derrière elle. Évitez : frontière (surchargé avec le contexte délimité du DDD).
Adaptateur : une chose concrète qui satisfait une interface à une jointure. Décrit le rôle (quel emplacement il remplit), pas la substance (ce qu'il y a à l'intérieur).
Effet de levier : ce que les appelants obtiennent de la profondeur. Plus de capacité par unité d'interface qu'ils apprennent. Une seule implémentation est remboursée sur N sites d'appel et M tests.
Localité : ce que les mainteneurs obtiennent de la profondeur. Les changements, les bogues, les connaissances et la vérification se concentrent en un seul endroit plutôt que de se propager chez les appelants. Corrigé 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 │ ← Seulement des passages
└─────────────────────────────────┘
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 jointures internes (privées à son implémentation, utilisées par ses propres tests) ainsi qu'une jointure externe à son interface.
- Le test de suppression. Imaginez supprimer le module. Si la complexité disparaît, c'était un simple passage. 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 jointure. Si vous voulez tester au-delà de l'interface, le module a probablement la mauvaise forme.
- Un adaptateur signifie une jointure hypothétique. Deux adaptateurs signifient une jointure réelle. N'introduisez pas une jointure à moins que quelque chose ne varie réellement à travers elle.
Concevoir pour la testabilité
De bonnes interfaces rendent les tests naturels :
-
Acceptez les dépendances, ne les créez pas.
// Testable function processOrder(order, paymentGateway) {} // Difficile à tester function processOrder(order) { const gateway = new StripeGateway(); } -
Retournez des résultats, ne produisez pas d'effets secondaires.
// Testable function calculateDiscount(cart): Discount {} // Difficile à tester function applyDiscount(cart): void { cart.total -= discount; } -
Petite surface. 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 Jointure est l'endroit où se trouve l'Interface d'un Module.
- Un Adaptateur se trouve à une Jointure et satisfait l'Interface.
- La Profondeur produit un Effet de levier pour les appelants et une Localité pour les mainteneurs.
Cadres rejetés
- La profondeur comme rapport entre le nombre de lignes d'implémentation et le nombre de lignes d'interface (Ousterhout) : récompense le gonflement de l'implémentation. Nous utilisons plutôt la profondeur comme effet de levier.
- « Interface » comme le mot-clé
interfacede TypeScript ou les méthodes publiques d'une classe : trop étroit : l'interface ici inclut tous les faits qu'un appelant doit connaître. - « Frontière » : surchargé avec le contexte délimité du DDD. Dites jointure ou interface.
Aller plus loin
- Approfondir un cluster étant donné ses dépendances, voir DEEPENING.md : catégories de dépendances, discipline de jointure et test de substitution plutôt que de couche.
- Explorer des interfaces alternatives, voir DESIGN-IT-TWICE.md : lancer des sous-agents parallèles pour concevoir l'interface de plusieurs manières radicalement différentes, puis comparer sur la profondeur, la localité et le placement de la jointure.