Codebase Design
Entwickeln Sie tiefe Module: viel Verhalten hinter einer kleinen Schnittstelle, platziert an einer sauberen Naht, testbar durch diese Schnittstelle. Verwenden Sie diese Sprache und diese Prinzipien überall dort, wo Code entworfen oder umstrukturiert wird. Ziel ist Hebelwirkung für Aufrufer, Lokalität für Wartende und Testbarkeit für alle.
Glossar
Verwenden Sie diese Begriffe exakt: Ersetzen Sie nicht "Komponente", "Service", "API" oder "Grenze". Konsistente Sprache ist der springende Punkt.
Modul: alles mit einer Schnittstelle und einer Implementierung. Bewusst skalierungsagnostisch: eine Funktion, Klasse, ein Paket oder ein bereichsübergreifender Schnitt. Vermeiden Sie: Einheit, Komponente, Service.
Interface: alles, was ein Aufrufer wissen muss, um das Modul korrekt zu verwenden: die Typsignatur, aber auch Invarianten, Reihenfolge-Einschränkungen, Fehlermodi, erforderliche Konfiguration und Leistungscharakteristiken. Vermeiden Sie: API, Signatur (zu eng, beziehen sich nur auf die typebene Oberfläche).
Implementierung: was sich innerhalb eines Moduls befindet, sein Codekörper. Abgrenzung zu Adapter: ein Ding kann ein kleiner Adapter mit einer großen Implementierung sein (ein Postgres-Repo) oder ein großer Adapter mit einer kleinen Implementierung (ein In-Memory-Fake). Greifen Sie zu "Adapter", wenn die Naht das Thema ist; ansonsten "Implementierung".
Tiefe: Hebelwirkung an der Schnittstelle. Die Menge an Verhalten, die ein Aufrufer (oder ein Test) pro Einheit der Schnittstelle, die er lernen muss, ausüben kann. Ein Modul ist tief, wenn eine große Menge an Verhalten hinter einer kleinen Schnittstelle steckt, und flach, wenn die Schnittstelle fast so komplex ist wie die Implementierung.
Naht (Michael Feathers): ein Ort, an dem Sie Verhalten ändern können, ohne an diesem Ort zu bearbeiten; der Ort, an dem die Schnittstelle eines Moduls lebt. Wo die Naht platziert wird, ist eine eigene Designentscheidung, getrennt von dem, was dahinter liegt. Vermeiden Sie: Grenze (überladen mit DDDs Bounded Context).
Adapter: ein konkretes Etwas, das eine Schnittstelle an einer Naht erfüllt. Beschreibt die Rolle (welchen Slot es füllt), nicht die Substanz (was drin ist).
Hebelwirkung: was Aufrufer aus Tiefe gewinnen. Mehr Fähigkeit pro Einheit Schnittstelle, die sie lernen. Eine Implementierung zahlt sich über N Aufrufstellen und M Tests aus.
Lokalität: was Wartende aus Tiefe gewinnen. Änderungen, Fehler, Wissen und Verifikation konzentrieren sich an einem Ort, anstatt sich über Aufrufer zu verteilen. Einmal korrigiert, überall korrigiert.
Tief vs. flach
Tiefes Modul = kleine Schnittstelle + viel Implementierung:
┌─────────────────────┐
│ Kleine Schnittstelle │ ← Wenige Methoden, einfache Parameter
├─────────────────────┤
│ │
│ Tiefe Implementierung│ ← Komplexe Logik verborgen
│ │
└─────────────────────┘
Flaches Modul = große Schnittstelle + wenig Implementierung (vermeiden):
┌─────────────────────────────────┐
│ Große Schnittstelle │ ← Viele Methoden, komplexe Parameter
├─────────────────────────────────┤
│ Dünne Implementierung │ ← Leitet nur weiter
└─────────────────────────────────┘
Fragen Sie beim Entwerfen einer Schnittstelle:
- Kann ich die Anzahl der Methoden reduzieren?
- Kann ich die Parameter vereinfachen?
- Kann ich mehr Komplexität im Inneren verstecken?
Prinzipien
- Tiefe ist eine Eigenschaft der Schnittstelle, nicht der Implementierung. Ein tiefes Modul kann intern aus kleinen, mockbaren, austauschbaren Teilen bestehen; sie sind nur nicht Teil der Schnittstelle. Ein Modul kann interne Nähte (privat für seine Implementierung, verwendet von seinen eigenen Tests) sowie die externe Naht an seiner Schnittstelle haben.
- Der Löschtest. Stellen Sie sich vor, Sie löschen das Modul. Wenn Komplexität verschwindet, war es ein Durchgang. Wenn Komplexität bei N Aufrufern wieder auftaucht, hat es seinen Wert verdient.
- Die Schnittstelle ist die Testoberfläche. Aufrufer und Tests überqueren dieselbe Naht. Wenn Sie hinter die Schnittstelle testen wollen, hat das Modul wahrscheinlich die falsche Form.
- Ein Adapter bedeutet eine hypothetische Naht. Zwei Adapter bedeuten eine echte. Führen Sie keine Naht ein, wenn nicht tatsächlich etwas darüber variiert.
Design für Testbarkeit
Gute Schnittstellen machen Tests natürlich:
-
Abhängigkeiten akzeptieren, nicht erzeugen.
// Testbar function processOrder(order, paymentGateway) {} // Schwer testbar function processOrder(order) { const gateway = new StripeGateway(); } -
Ergebnisse zurückgeben, keine Seiteneffekte erzeugen.
// Testbar function calculateDiscount(cart): Discount {} // Schwer testbar function applyDiscount(cart): void { cart.total -= discount; } -
Kleine Oberfläche. Weniger Methoden = weniger benötigte Tests. Weniger Parameter = einfachere Testeinrichtung.
Beziehungen
- Ein Modul hat genau ein Interface (die Oberfläche, die es Aufrufern und Tests präsentiert).
- Tiefe ist eine Eigenschaft eines Moduls, gemessen an seinem Interface.
- Eine Naht ist der Ort, an dem das Interface eines Moduls lebt.
- Ein Adapter sitzt an einer Naht und erfüllt das Interface.
- Tiefe erzeugt Hebelwirkung für Aufrufer und Lokalität für Wartende.
Abgelehnte Framings
- Tiefe als Verhältnis von Implementierungszeilen zu Schnittstellenzeilen (Ousterhout): belohnt das Aufblähen der Implementierung. Wir verwenden stattdessen Tiefe-als-Hebelwirkung.
- "Interface" als das TypeScript
interface-Schlüsselwort oder die öffentlichen Methoden einer Klasse: zu eng: Interface umfasst hier jede Tatsache, die ein Aufrufer wissen muss. - "Grenze": überladen mit DDDs Bounded Context. Sagen Sie stattdessen Naht oder Schnittstelle.
Tiefer gehen
- Vertiefen eines Clusters bei gegebenen Abhängigkeiten, siehe DEEPENING.md: Abhängigkeitskategorien, Nahtdisziplin und Ersetzen-statt-Schichten-Testen.
- Alternative Schnittstellen erkunden, siehe DESIGN-IT-TWICE.md: parallele Sub-Agenten starten, um die Schnittstelle auf mehrere radikal unterschiedliche Weisen zu entwerfen und dann hinsichtlich Tiefe, Lokalität und Nahtplatzierung zu vergleichen.