Be Software Architecture Wise
Be Wise Apps

Read in English · Leer en español · Lire en français

Abschnitt 3.4 von 3.8 · Mittelstufe · 25 Min. Lesezeit

Abhängigkeiten steuern: Richtung, Zyklen und Stabilität

Videolektion

5 Min. · auf Englisch gesprochen, mit Untertiteln
Nur anhören (Audio)
Transkript lesen

Jede Abhängigkeit hat eine Richtung, und diese Richtung entscheidet, wer durch wessen Änderungen gestört wird. In dieser Lektion lernen Sie drei Prinzipien für Abhängigkeiten zwischen Komponenten kennen, die Metriken dahinter und wie Sie sie automatisch prüfen.

Denken Sie an ein Gebäude. Die oberen Stockwerke ruhen auf den unteren, und das Fundament ruht auf nichts darüber. Sie können das oberste Stockwerk renovieren, ohne das Fundament anzurühren. Aber wenn Sie unter dem Fundament graben, ist alles darüber in Gefahr. Stellen Sie sich nun zwei Stockwerke vor, die sich gegenseitig tragen. Sie könnten keines von beiden je umbauen. Gute Software setzt Teile, die sich oft ändern, auf solide, selten geänderte Teile und lässt nie zwei Teile sich gegenseitig in einer Schleife tragen.

Zeichnen Sie eine Abhängigkeit als Pfeil von dem Modul, das etwas braucht, zu dem Modul, das es braucht: Checkout zu Preisgestaltung. Ändert sich die Preisgestaltung, muss sich der Checkout womöglich ändern. Ändert sich der Checkout, ist das der Preisgestaltung gleich. Änderungen laufen entgegen den Pfeilen. Zwischen Services zeigt sich Richtung als Wissen. Ruft Bestellungen die API der Abrechnung auf, hängt Bestellungen von der Abrechnung ab. Veröffentlicht Bestellungen ein Ereignis, das die Abrechnung konsumiert, kehrt sich der Pfeil um, und Bestellungen weiß nicht einmal, dass es die Abrechnung gibt.

Robert C. Martins erstes Prinzip ist das Acyclic Dependencies Principle: keine Zyklen zwischen Komponenten. Hier ruft Bestellungen die Benachrichtigungen auf, um eine Bestätigung zu senden, und Benachrichtigungen ruft in Bestellungen zurück, um die Bestellung zu lesen. Keine lässt sich ohne die andere bauen, testen oder releasen. Der erste Weg kehrt eine Abhängigkeit um. Benachrichtigungen deklariert eine kleine Schnittstelle, Order Lookup, die genau beschreibt, was es braucht, und Bestellungen implementiert sie. Benachrichtigungen hängt jetzt von nichts in Bestellungen ab. Die Schnittstelle gehört dem Modul, das sie nutzt. Der zweite Weg löst das, was beide Seiten brauchen, hier die Bestellereignisse, in eine neue Komponente heraus, von der beide abhängen. Sie hängt von keiner ab und ist daher stabiler als beide.

Das zweite Prinzip ist das Stable Dependencies Principle: Abhängigkeiten in Richtung der Stabilität. Von einer Komponente, die leicht zu ändern sein soll, sollte keine abhängen, die schwer zu ändern ist. Martin misst das als Instabilität: die ausgehenden Abhängigkeiten geteilt durch alle Abhängigkeiten, eingehende plus ausgehende. Null bedeutet nur eingehende Abhängigkeiten: maximal stabil und vielen gegenüber verantwortlich. Eins bedeutet nur ausgehende: Nichts hängt davon ab, also kann sie sich frei ändern. Entlang jedes Pfeils sollte die Instabilität also sinken. Jede Komponente sollte von Komponenten abhängen, die stabiler sind als sie selbst.

Das dritte Prinzip ist das Stable Abstractions Principle: Eine Komponente sollte so abstrakt sein, wie sie stabil ist. Die Abstraktheit ist der Anteil ihrer Klassen, die abstrakte Klassen oder Schnittstellen sind. Tragen Sie die Abstraktheit gegen die Instabilität auf, erhalten Sie dieses Quadrat. Die Diagonale ist die Hauptreihe: stabil und abstrakt oben links, instabil und konkret unten rechts. Die Distanz zu ihr reicht von null, auf der Linie, bis eins, in einer entfernten Ecke. Nahe unten links liegt die Zone des Schmerzes: stabil, weil viel davon abhängt, aber konkret, sodass Änderungen nötig und schmerzhaft sind. Ein Datenbankschema, das jedes Modul direkt liest, liegt hier. Nahe oben rechts liegt die Zone der Nutzlosigkeit: Abstraktionen, von denen nichts abhängt, oft Schnittstellen, die nach einem Refactoring übrig geblieben sind.

Probieren wir es an einem Shop aus. Seine Komponente payments API hat sechs eingehende Abhängigkeiten und eine ausgehende, die Instabilität ist also ein Siebtel, etwa null Komma eins vier. Vier ihrer fünf Klassen sind Schnittstellen. Sie liegt fast genau auf der Hauptreihe. Orders core hat acht eingehende und zwei ausgehende, die Instabilität ist also null Komma zwei. Aber nur zwei ihrer zwanzig Klassen sind abstrakt, daher beträgt ihre Distanz zur Hauptreihe null Komma sieben. Stabil, aber konkret: Sie liegt in der Zone des Schmerzes. Die Weboberfläche hängt von neun anderen ab, und nichts hängt von ihr ab. Vollständig instabil, vollständig konkret und genau auf der Linie. Die Abhilfe verschiebt die Typen, die die abhängigen Klassen von orders core nutzen, in eine kleine, überwiegend abstrakte Komponente und lässt den konkreten Code in einer separaten. Beide landen bei einer Distanz von null Komma zwei fünf. Die Zahlen sind Anlass hinzusehen, keine Zielwerte.

Schichtenbildung ist die älteste Abhängigkeitsregel. Bei strikter Schichtenbildung darf jede Schicht nur die direkt darunterliegende nutzen. Jede Schicht verbirgt alles unter ihr, aber mittlere Schichten reichen Aufrufe womöglich nur weiter. Bei gelockerter Schichtenbildung darf eine Schicht jede Schicht unter ihr nutzen. Das beseitigt den Weiterleitungscode, aber eine Änderung ganz unten kann jede Schicht darüber betreffen. Welche Regeln Sie auch wählen, prüfen Sie sie automatisch. Werkzeuge wie ArchUnit für Java, dependency-cruiser für JavaScript und import-linter für Python laufen im Build und lassen ihn fehlschlagen, wenn eine Regel verletzt wird.

Der vollständige Abschnitt enthält Beispielregeln für jedes Werkzeug und eine Entscheidungshilfe, um eine unerwünschte Abhängigkeit zu beheben. Als Nächstes wenden wir in Abschnitt drei Punkt fünf die SOLID-Prinzipien auf Komponenten und Services an. Bis gleich.

Was Sie wissen müssen

Warum die Richtung zählt

Eine Abhängigkeit besteht, wenn ein Modul ein anderes braucht, um zu kompilieren, zu laufen oder sich korrekt zu verhalten. Zeichnen Sie sie als Pfeil von dem Modul, das abhängt, zu dem Modul, von dem es abhängt: Checkout zu Preisgestaltung. Der Pfeil sagt etwas Wichtiges über Änderungen aus. Ändert die Preisgestaltung ihre Schnittstelle oder ihr Verhalten, muss sich der Checkout womöglich ändern; ändert sich der Checkout, ist das der Preisgestaltung gleich. Änderungen laufen entgegen der Pfeilrichtung. Die Richtung jeder Abhängigkeit ist also eine Entscheidung darüber, wer durch wessen Änderungen gestört wird.

Architekten befassen sich mit zwei Arten von Richtung, die man leicht verwechselt. Der Kontrollfluss ist, wer wen zur Laufzeit aufruft. Die Quellcode-Abhängigkeit ist, wer wen importiert, referenziert oder gegen wen kompiliert. Meist zeigen beide in dieselbe Richtung, aber das muss nicht so sein. Ein Framework ruft Ihren Code auf, ohne von ihm abzuhängen (Abschnitt 0.3 nannte das Inversion of Control), und mit einer Schnittstelle kann ein übergeordnetes Modul ein untergeordnetes aufrufen, während die Quellcode-Abhängigkeit in die andere Richtung zeigt. Diese Freiheit, die Richtung der Quellcode-Abhängigkeit unabhängig vom Kontrollfluss zu wählen, ist das mächtigste Werkzeug dieses Abschnitts.

Zwischen Services zeigt sich Richtung als Wissen. Ruft Bestellungen die API der Abrechnung auf, hängt Bestellungen von der Abrechnung ab und muss sich ändern, wenn sich deren API ändert. Veröffentlicht Bestellungen ein Ereignis, das die Abrechnung konsumiert, kehrt sich die Abhängigkeit um: Die Abrechnung hängt vom Ereignisvertrag von Bestellungen ab, und Bestellungen weiß nicht, dass es die Abrechnung gibt (Abschnitt 3.2). Die Wahl zwischen einem Aufruf und einem Ereignis ist daher teilweise eine Wahl der Abhängigkeitsrichtung.

Das Acyclic Dependencies Principle

Robert C. Martin formulierte drei Prinzipien für Abhängigkeiten zwischen Komponenten, zuerst Mitte der 1990er-Jahre in Artikeln und Aufsätzen und später in Agile Software Development: Principles, Patterns, and Practices (2002) und Clean Architecture (2017). Das erste, das Acyclic Dependencies Principle (ADP), besagt, dass der Abhängigkeitsgraph zwischen Komponenten keine Zyklen enthalten darf: Er muss ein gerichteter azyklischer Graph sein.

Ein Abhängigkeitszyklus wie „Bestellungen hängt von Benachrichtigungen ab, Benachrichtigungen von Kunden und Kunden von Bestellungen“ macht aus drei Komponenten eine. Keine lässt sich ohne die anderen kompilieren oder testen; eine Änderung an einer kann sich durch die Schleife bis zu ihr selbst zurück fortpflanzen; keine kann eigenständig ausgeliefert werden; und niemand kann sagen, welche zuerst zu bauen ist. Martin führt das Prinzip mit dem ein, was er das Morning-after-Syndrom nennt: Sie gehen mit funktionierendem Code nach Hause und stellen am nächsten Tag fest, dass er kaputt ist, weil jemand anderes etwas geändert hat, wovon Sie abhängen. Sein Heilmittel ist, das System in getrennt releaste Komponenten aufzuteilen, und das funktioniert nur, wenn die Abhängigkeiten zwischen ihnen keine Zyklen enthalten. Zyklen machen es außerdem unmöglich, eine Komponente später als Bibliothek oder Service herauszulösen, weil sie die ganze Schleife mitzieht.

Das Java Platform Module System erzwingt ADP zwischen Modulen: Die Java Language Specification macht es zu einem Kompilierfehler, wenn ein Modul direkt oder indirekt von sich selbst abhängt. Die meisten anderen Sprachen erlauben Zyklen zwischen Paketen, daher prüfen Teams sie mit Werkzeugen. Zyklen zwischen Klassen innerhalb einer kleinen Komponente sind weniger schädlich, und unter anderem Martin Fowler toleriert sie dort; am wichtigsten ist das Prinzip zwischen Komponenten, die verschiedene Leute bauen und releasen.

Zyklen auflösen

Jeder Zyklus lässt sich auflösen, und es gibt zwei Standardtechniken. Beide zeigt das Diagramm unten für einen Zyklus, in dem Bestellungen die Benachrichtigungen aufruft, um eine Bestätigung zu senden, und Benachrichtigungen in Bestellungen zurückruft, um die Details der Bestellung zu lesen.

Abhängigkeitsumkehr. Benachrichtigungen deklariert eine Schnittstelle, die genau beschreibt, was es braucht, etwa OrderLookup mit einer Methode, und Bestellungen implementiert sie. Der Pfeil von Benachrichtigungen zu Bestellungen zeigt nun in die andere Richtung: Bestellungen hängt von Benachrichtigungen ab (um es aufzurufen und seine Schnittstelle zu implementieren), und Benachrichtigungen hängt von nichts in Bestellungen ab. Das ist Abhängigkeitsumkehr, die Abschnitt 3.5 als das D in SOLID behandelt. Das entscheidende Detail ist die Eigentümerschaft: Die Schnittstelle gehört dem Modul, das sie nutzt, nicht dem, das sie implementiert.

Eine neue Komponente herauslösen. Finden Sie heraus, was beide Seiten tatsächlich voneinander brauchen, oft ein kleines gemeinsames Modell oder ein Vertrag, und verschieben Sie es in eine neue Komponente, von der beide abhängen. Hier veröffentlicht Bestellungen einen Ereignistyp, der in einer Komponente Bestellereignisse definiert ist, und Benachrichtigungen liest die Bestelldetails aus dem Ereignis, statt zurückzurufen. Von der neuen Komponente hängen beide ab, und sie hängt von keiner ab, ist also stabiler als jede von ihnen: genau das, was das nächste Prinzip verlangt.

Zwei Wege, einen Zyklus aufzulösen

Ein Abhängigkeitszyklus zwischen Bestellungen und Benachrichtigungen, zweifach aufgelöst: durch Abhängigkeitsumkehr und durch eine neue KomponenteZyklusBestell.BenachrichtigungensendetBestelldatenkeine lässt sich ohne dieandere bauen, testen, releasenWeg 1: Abhängigkeit umkehrenBestell.OrderLookupBenachrichtigungensetzt umSchnittstelle von BenachrichtigungenWeg 2: Komponente herauslösenBestell.Benachr.-serviceBestellereign.(gem. Modell)beide nutzen eine neue,stabilere KomponenteWeg 1: Bestellungen ruft Benachr. auf, das seinen Bedarf als Schnittstelle nennt; Bestellungen implementiert sie.Weg 2: Der gemeinsame Teil kommt in eine eigene Komponente; kein Lesen mehr aus Bestellungen.Pfeile zeigen vom abhängigen Modul zu dem Modul, von dem es abhängt. Gestrichelt: implementiert.
Ein Zyklus lässt sich auflösen, indem man eine Abhängigkeit über eine Schnittstelle umkehrt, die ihrem Nutzer gehört, oder indem man das, was beide Seiten teilen, in eine neue Komponente herauslöst.

Das Stable Dependencies Principle und Instabilität

Manche Komponenten ändern sich oft (Benutzeroberflächen, Integrationen mit Partnern, neue Features); andere sollten sich selten ändern (zentrale Domänenkonzepte, gemeinsame Verträge, der Umgang mit Geld). Das Stable Dependencies Principle (SDP) besagt: Abhängigkeiten in Richtung der Stabilität. Von einer Komponente, die leicht zu ändern sein soll, sollte keine Komponente abhängen, die schwer zu ändern ist, sonst wird sie ebenfalls schwer zu ändern.

Martin misst Stabilität über Abhängigkeiten, nicht darüber, wie oft der Code bearbeitet wird. Eine Komponente, von der viele andere abhängen, ist stabil in dem Sinne, dass sie teuer zu ändern ist: Jede Änderung riskiert, ihre abhängigen Komponenten zu brechen. Eine Komponente, die von vielen anderen abhängt und keine abhängigen Komponenten hat, ist instabil: Nichts hindert sie daran, sich zu ändern, und ihre Abhängigkeiten können sie dazu zwingen. Mit der afferenten Kopplung (Ca, eingehend) und der efferenten Kopplung (Ce, ausgehend) aus Abschnitt 3.2 ist die Instabilität:

I = Ce / (Ca + Ce)

I reicht von 0 bis 1. I = 0 bedeutet nur eingehende Abhängigkeiten: maximal stabil, vielen gegenüber verantwortlich, von nichts abhängig. I = 1 bedeutet nur ausgehende Abhängigkeiten: maximal instabil, nichts hängt davon ab. Als Metrik formuliert besagt das SDP, dass I entlang jedes Abhängigkeitspfeils abnehmen sollte: Jede Komponente sollte von Komponenten mit einem niedrigeren I als ihrem eigenen abhängen. Eine Abhängigkeit von einer stabilen Komponente (I = 0,1) zu einer volatilen (I = 0,8) ist ein Warnsignal; die übliche Lösung ist wiederum Abhängigkeitsumkehr, also eine Schnittstelle an einer stabilen Stelle.

Das Stable Abstractions Principle und die Hauptreihe

Stabilität hat ihren Preis: Eine Komponente mit I nahe 0 ist schwer zu ändern. Besteht sie aus viel konkretem Code, wird das System starr. Das Stable Abstractions Principle (SAP) löst das: Eine Komponente sollte so abstrakt sein, wie sie stabil ist. Stabile Komponenten sollten überwiegend aus Schnittstellen und abstrakten Typen bestehen, damit sich ihr Verhalten durch neue Implementierungen an anderer Stelle erweitern lässt, ohne sie zu ändern; instabile Komponenten dürfen konkret sein, weil sie ohnehin leicht zu ändern sind. SDP und SAP zusammen sind das Dependency Inversion Principle, angewandt auf ganze Komponenten: Abhängigkeiten zeigen zur Stabilität, und Stabilität geht mit Abstraktion einher.

Die Abstraktheit wird gemessen als A = Na / Nc, wobei Na die Zahl der abstrakten Klassen und Schnittstellen in der Komponente ist und Nc die Gesamtzahl der Klassen. A = 0 bedeutet vollständig konkret; A = 1 bedeutet nichts als Abstraktionen. Trägt man A gegen I auf, ergibt sich ein Quadrat. Die Linie von (I = 0, A = 1) bis (I = 1, A = 0) ist die Hauptreihe: stabil und abstrakt an einem Ende, instabil und konkret am anderen, mit ausgewogenen Mischungen dazwischen. Die Distanz zu ihr ist:

D = |A + I - 1|

D reicht von 0 (auf der Linie) bis 1 (in einer entfernten Ecke). Die Ecke nahe (0, 0) ist die Zone des Schmerzes: stabil, weil viel davon abhängt, und konkret, sodass jede Änderung sowohl nötig als auch schmerzhaft ist. Ein Datenbankschema, das jedes Modul direkt liest, liegt hier. Martin merkt an, dass manche Komponenten harmlos dorthin gehören, etwa eine ausgereifte konkrete Hilfsbibliothek, die sich nie ändern muss; die Zone schmerzt nur bei volatilen Komponenten. Die Ecke nahe (1, 1) ist die Zone der Nutzlosigkeit: Abstraktionen, von denen nichts abhängt, typischerweise Schnittstellen, die nach einem Refactoring übrig geblieben sind oder für eine Erweiterung entworfen wurden, die nie kam.

Die Hauptreihe

Abstraktheit aufgetragen gegen Instabilität, mit der Hauptreihe von (0, 1) bis (1, 0), der Zone des Schmerzes nahe (0, 0) und der Zone der Nutzlosigkeit nahe (1, 1), und drei eingetragenen Komponenten0101Instabilität I = Ce / (Ca + Ce)Abstraktheit ASchmerz-zoneZone derNutzlosigkeitHauptreihe: A + I = 1Komponentenpayments-apiI 0,14, A 0,80D 0,06orders-coreI 0,20, A 0,10D 0,70web-uiI 1,00, A 0,00D 0,00roter Punkt: nebender Hauptreihepayments-apiorders-coreweb-ui
Komponenten nahe der Hauptreihe halten Abstraktheit und Stabilität im Gleichgewicht; die Zone des Schmerzes enthält stabilen konkreten Code, die Zone der Nutzlosigkeit Abstraktionen, die niemand nutzt.

Ausgearbeitetes Beispiel

Ausgearbeitetes Beispiel: I, A und D für drei Komponenten berechnen

Ein Shop hat drei Komponenten. Nach Martins Definition zählt Ca die Klassen außerhalb der Komponente, die von Klassen in ihr abhängen, und Ce die Klassen in ihr, die von Klassen außerhalb abhängen. Die Zahlen stammen aus einem Abhängigkeitswerkzeug.

payments-api (5 Klassen, davon 4 Schnittstellen)Ca = 6, Ce = 1. I = 1 / (6 + 1) = 0,14. A = 4 / 5 = 0,80. D = |0,80 + 0,14 - 1| = 0,06.
orders-core (20 Klassen, 2 abstrakt)Ca = 8, Ce = 2. I = 2 / (8 + 2) = 0,20. A = 2 / 20 = 0,10. D = |0,10 + 0,20 - 1| = 0,70.
web-ui (12 Klassen, keine abstrakt)Ca = 0, Ce = 9. I = 9 / (0 + 9) = 1,00. A = 0 / 12 = 0,00. D = |0 + 1 - 1| = 0,00.
SDP entlang der Pfeile prüfenweb-ui (1,00) hängt von orders-core (0,20) und von payments-api (0,14) ab; orders-core (0,20) hängt von payments-api (0,14) ab. I nimmt entlang jedes Pfeils ab, also gilt das SDP.
Diagnoseorders-core hat D = 0,70: stabil (viele abhängige Klassen), aber konkret. Ändern sich seine Regeln oft, gefährdet jede Änderung acht abhängige Klassen. Es liegt in der Zone des Schmerzes.
Abhilfe und NeuberechnungDie vier Typen, die seine abhängigen Klassen nutzen, in orders-api herauslösen (3 Schnittstellen und 1 Werttyp: A = 0,75; Ca ist 8 oder mehr, da nun auch die implementierenden Klassen davon abhängen, und Ce = 0, also I = 0; D = |0,75 + 0 - 1| = 0,25). Die übrigen 16 konkreten Klassen werden zu orders-impl, von dem nur die Start-Verdrahtung abhängt (Ca = 1) und das von orders-api und zwei weiteren abhängt (Ce = 3): I = 3 / 4 = 0,75, A = 0, D = 0,25.

Die Abhilfe rückt beide Teile viel näher an die Hauptreihe (jeweils von 0,70 auf 0,25). Abhängige Klassen verlassen sich nun auf eine kleine, stabile, überwiegend abstrakte Komponente, und der volatile konkrete Code liegt dort, wo er sich günstig ändern lässt.

Die Zahlen sind Heuristiken, keine Zielwerte. Ein D von 0,70 ist ein Grund hinzusehen, kein Beweis für ein Problem; würde sich orders-core nie ändern, richtete es dort, wo es ist, keinen Schaden an.

Strikte und gelockerte Schichtenbildung

Schichtenbildung ist die älteste und häufigste Abhängigkeitsregel: Module werden in Schichten angeordnet, etwa Präsentation, Anwendung oder Domäne und Persistenz, und jede Schicht darf nur Schichten unter ihr nutzen. Untere Schichten wissen nichts über höhere, sodass sich eine neue Benutzeroberfläche hinzufügen lässt, ohne die Domäne zu ändern. Schichtenbildung ist ein eigener Architekturstil (Abschnitt 5.2); hier interessiert die Abhängigkeitsregel, die sie vorgibt.

Bei strikter (geschlossener) Schichtenbildung darf jede Schicht nur die unmittelbar darunterliegende Schicht nutzen. Jede Schicht verbirgt dann alles unter ihr, und das Ersetzen einer unteren Schicht betrifft nur eine andere Schicht; der Preis ist durchreichender Code, bei dem die mittlere Schicht bloß Aufrufe weiterleitet, zu denen sie nichts beizutragen hat. Bei gelockerter (offener) Schichtenbildung darf eine Schicht jede Schicht unter ihr nutzen. Das beseitigt den Boilerplate-Code, bedeutet aber, dass eine Änderung an der untersten Schicht jede Schicht darüber betreffen kann. Viele reale Systeme mischen beides und markieren bestimmte Schichten als offen, etwa eine gemeinsame Logging- oder Hilfsschicht, die alle nutzen dürfen.

Die klassische Schichtenbildung hat eine berüchtigte Schwäche: Die Domänenschicht hängt von der Persistenzschicht ab, sodass die wichtigsten Geschäftsregeln von Datenbankdetails abhängen. Hexagonale, Onion- und Clean Architecture (Abschnitt 5.6) wenden Abhängigkeitsumkehr an, um das zu beheben: Die Domäne besitzt Schnittstellen wie OrderRepository, und der Persistenzcode implementiert sie, sodass alle Quellcode-Abhängigkeiten nach innen zur Domäne zeigen.

Strikte und gelockerte Schichtenbildung

Strikte Schichtenbildung erlaubt Aufrufe nur in die direkt tiefere Schicht; gelockerte erlaubt Aufrufe in jede tiefere SchichtStrikt (geschlossen)PräsentationAnwendung / DomänePersistenzjede Schicht nutzt nur die nächsttiefereGelockert (offen)PräsentationAnwendung / DomänePersistenzSprungjede Schicht darf jede tiefere nutzen
Strikte Schichtenbildung erlaubt Aufrufe nur in die nächsttiefere Schicht; gelockerte Schichtenbildung erlaubt einer Schicht auch, darunterliegende Schichten zu überspringen.

Abhängigkeitsregeln automatisch prüfen

Abhängigkeitsregeln, die nur in einem Wiki stehen, werden innerhalb von Wochen gebrochen. Der verlässliche Weg, sie einzuhalten, ist, sie als ausführbare Prüfungen zu formulieren, die im Build laufen und ihn fehlschlagen lassen, wenn eine Regel verletzt wird. Eine solche Prüfung ist eine architektonische Fitness Function, eine Idee, die in Abschnitt 2.8 eingeführt und in Abschnitt 11.3 weiterentwickelt wird. Drei weit verbreitete Open-Source-Werkzeuge decken die wichtigsten Ökosysteme ab.

ArchUnit (Java und andere JVM-Sprachen) lässt Sie Architekturregeln als gewöhnliche Unit-Tests schreiben: welche Pakete auf welche zugreifen dürfen, Schichtdefinitionen und Zyklenfreiheit zwischen Teilen der Codebasis. dependency-cruiser (JavaScript und TypeScript) analysiert Importe und prüft sie gegen Regeln in einer Konfigurationsdatei, und seine Startkonfiguration verbietet bereits zirkuläre Abhängigkeiten. import-linter (Python) prüft in einer Konfigurationsdatei deklarierte Contracts; zu seinen Contract-Typen gehören verbotene Importe, Schichten, Unabhängigkeit zwischen benachbarten Modulen, geschützte Module und azyklische Geschwistermodule.

@ArchTest
static final ArchRule layers = layeredArchitecture()
    .consideringAllDependencies()
    .layer("Web").definedBy("..web..")
    .layer("Domain").definedBy("..domain..")
    .layer("Persistence").definedBy("..persistence..")
    .whereLayer("Web").mayNotBeAccessedByAnyLayer()
    .whereLayer("Domain").mayOnlyBeAccessedByLayers("Web", "Persistence")
    .whereLayer("Persistence").mayNotBeAccessedByAnyLayer();

@ArchTest
static final ArchRule noCycles =
    slices().matching("com.shop.(*)..").should().beFreeOfCycles();
ArchUnit (Java): eine Schichtenregel und eine Zyklenprüfung, als Tests geschrieben.
// .dependency-cruiser.js
module.exports = {
  forbidden: [
    { name: "no-circular", severity: "error",
      from: {}, to: { circular: true } },
    { name: "domain-not-to-db", severity: "error",
      comment: "Domain code must use repository interfaces, not the database",
      from: { path: "^src/domain" },
      to: { path: "^src/db" } },
  ],
};
dependency-cruiser (JavaScript und TypeScript): Zyklen verbieten und verhindern, dass die Domäne die Datenbankschicht importiert.
# .importlinter
[importlinter]
root_package = shop

[importlinter:contract:layers]
name = Web above domain above adapters
type = layers
layers =
    shop.web
    shop.domain
    shop.adapters

[importlinter:contract:features-independent]
name = Feature packages do not import each other
type = independence
modules =
    shop.catalogue
    shop.ordering
    shop.payments
import-linter (Python): ein Schichten-Contract und ein Unabhängigkeits-Contract, geprüft durch Ausführen von lint-imports in der Pipeline.

Beachten Sie, dass die ArchUnit-Regel oben der Persistenz den Zugriff auf die Domäne erlaubt, aber nicht umgekehrt: die eben beschriebene domänenzentrierte Anordnung, nicht die klassische Schichtenbildung. Solche Regeln sind schnell hinzugefügt, laufen in Sekunden und machen aus einem Architekturdiagramm etwas, das der Build verteidigt. Verletzt eine bestehende Codebasis eine Regel bereits, können die meisten Werkzeuge die aktuellen Verstöße als Baseline erfassen und nur bei neuen fehlschlagen, sodass die Regel ab dem Tag ihrer Einführung verhindert, dass es schlimmer wird.

Entscheidungshilfe

Entscheidungshilfe: was tun mit einer unerwünschten Abhängigkeit

Nutzen Sie dies, wenn eine Abhängigkeitsprüfung fehlschlägt oder ein Review eine Abhängigkeit findet, die in die falsche Richtung zeigt.

  1. Ist die Abhängigkeit Teil eines Zyklus zwischen Komponenten?
    JaLösen Sie zuerst den Zyklus auf (ADP): Lesen Sie unten weiter, um zu entscheiden, wie.
    NeinGehen Sie zur Frage nach der Stabilität.
  2. Braucht eine Seite von der anderen nur einen kleinen, klar umrissenen Dienst?
    JaKehren Sie die Abhängigkeit um: Die Seite, die den Dienst nutzt, besitzt eine Schnittstelle, und die andere Seite implementiert sie.
    NeinWeiter zur nächsten Frage.
  3. Hängen beide Seiten von einem gemeinsamen Modell, Vertrag oder Stück Logik ab?
    JaLösen Sie es in eine neue Komponente heraus, von der beide abhängen, und machen Sie diese Komponente klein, stabil und überwiegend abstrakt.
    NeinPrüfen Sie, ob die beiden Komponenten in Wahrheit eine sind: Führen Sie sie zusammen und teilen Sie sie dann entlang einer besseren Grenze neu auf (Abschnitt 3.1).
  4. Hängt eine stabile Komponente (niedriges I) von einer volatilen (hohes I) ab?
    JaDas verletzt das SDP. Legen Sie eine Schnittstelle in die stabile Komponente oder in eine neue stabile und verschieben Sie den volatilen Code dahinter.
    NeinDie Richtung ist akzeptabel. Halten Sie die Regel in einer Fitness Function fest, damit es so bleibt.

Wie Fachleute Abhängigkeiten steuern

Erfahrene Architekten haben ein Bild des Abhängigkeitsgraphen zwischen Komponenten und sehen es sich regelmäßig an, aus dem Code erzeugt statt von Hand gezeichnet. Sie entscheiden, welche Komponenten stabil sein sollen (Domänenkonzepte, gemeinsame Verträge) und welche volatil (Benutzeroberflächen, Integrationen), und lassen die Pfeile von volatil zu stabil zeigen. Neue Zyklen behandeln sie als Build-Fehler, nicht als Stilfragen. Und sie wenden dasselbe Denken über Services hinweg an: Gemeinsame Bibliotheken und gemeinsame Event-Schemas sind die stabilen Komponenten eines verteilten Systems und verdienen minimale Oberflächen und sorgfältige Versionierung, während die Interna jedes Service frei veränderbar bleiben.

Nichts davon erfordert, dass die Metriken genau berechnet werden. Ihr Wert liegt in den Fragen, die sie erzwingen: In welche Richtung sollte dieser Pfeil zeigen, was passiert mit allem, was davon abhängt, wenn sich das ändert, und ist diese Komponente so abstrakt, wie es die Zahl der Dinge verlangt, die von ihr abhängen? Abschnitt 3.5 geht weiter mit den SOLID-Prinzipien, die dieselben Ideen innerhalb von Komponenten und zwischen ihnen anwenden.

Prüfen Sie Ihr Verständnis

Warum müssen alle Komponenten in einem Abhängigkeitszyklus zusammen ausgeliefert werden?

Jede hängt direkt oder indirekt von allen anderen ab, sodass eine Änderung an einer Änderungen an den übrigen erfordern kann; keine lässt sich ohne die anderen bauen oder testen.

Eine Komponente hat Ca = 3 und Ce = 9. Wie hoch ist ihre Instabilität, und was bedeutet das?

I = 9 / 12 = 0,75. Sie hängt von viel mehr ab, als von ihr abhängt, ist also relativ instabil: leicht zu ändern, aber den Änderungen ihrer Abhängigkeiten ausgesetzt.

Was liegt in der Zone des Schmerzes, und wann schmerzt sie tatsächlich?

Komponenten, die zugleich stabil (viele abhängige Komponenten) und konkret (wenige Abstraktionen) sind. Es schmerzt, wenn sich eine solche Komponente oft ändern muss; eine konkrete Komponente, die sich nie ändert, richtet dort wenig Schaden an.

Was ist der Unterschied zwischen strikter und gelockerter Schichtenbildung?

Bei strikter Schichtenbildung darf eine Schicht nur die direkt darunterliegende Schicht nutzen; bei gelockerter darf sie jede Schicht unter ihr nutzen.

Weiter · 3.5SOLID im GroßenDie fünf SOLID-Prinzipien präzise erklärt, jedes in Python und dann auf Komponenten- und Service-Ebene gezeigt, dazu die Kohäsionsprinzipien REP, CCP und CRP, ihr Spannungsdreieck und die berechtigte Kritik an SOLID.Weiter →

Quellen und weiterführende Literatur

Haben Sie eine Frage zu diesem Abschnitt? ArchitectBot, der KI-Lernassistent des Kurses, antwortet zuerst aus dem Kurs.

Fragen: „Wie finde und löse ich die Abhängigkeitszyklen in einer großen Codebasis, ohne die Feature-Arbeit anzuhalten?“