Be Software Architecture Wise
Be Wise Apps

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

Abschnitt 3.2 von 3.8 · Mittelstufe · 24 Min. Lesezeit

Kopplung und Kohäsion

Videolektion

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

Kopplung und Kohäsion sind die beiden ältesten Maße für gute Struktur. In dieser Lektion lernen Sie ihre klassischen Stufen kennen, wie man Abhängigkeiten zählt, die zusätzlichen Arten von Kopplung zwischen Services und wie man sie lockert.

Denken Sie an zwei Nachbarn, die sich einen Garten teilen. Wenn sie sich nur gelegentlich ein Werkzeug leihen, kann jeder seinen eigenen Garten ohne Aufhebens umgestalten. Sie sind lose gekoppelt. Wenn sie sich einen Schuppen, einen Schlauch und einen Schlüssel unter dem Blumentopf teilen, braucht jede Änderung ein Gespräch. Sie sind eng gekoppelt. Denken Sie nun an eine Küchenschublade. Enthält sie nur Besteck, wissen Sie, wo Sie suchen müssen. Sie ist kohäsiv. Enthält sie Besteck, Batterien, Schnur und alte Kassenbons, ist sie es nicht. Gute Software hat kohäsive Teile, die lose miteinander gekoppelt sind.

Kopplung ist der Grad, in dem ein Modul von einem anderen abhängt, sodass eine Änderung am einen eine Änderung am anderen erzwingen kann. Kohäsion ist der Grad, in dem die Elemente innerhalb eines Moduls zusammengehören, weil sie einem einzigen Zweck dienen. Aber Kopplung ist kein Mangel. Ein System, dessen Teile nie voneinander abhingen, würde nichts Nützliches tun. Das Ziel ist, sie bewusst zu wählen: wenige Abhängigkeiten, über schmale und stabile Schnittstellen und so schwach, wie es die Aufgabe erlaubt.

Structured Design ordnete in den Siebzigerjahren die Arten, wie Module verbunden sein können, von der engsten links bis zur losesten rechts. Inhaltskopplung bedeutet, in die Interna eines anderen Moduls zu greifen, etwa wenn ein Service in die Datenbanktabellen eines anderen Service schreibt. Globale Kopplung bedeutet, globale Daten oder ein Datenbankschema gemeinsam zu nutzen. Externe Kopplung bedeutet, ein vorgegebenes Format zu teilen, etwa das Dateilayout eines Partners. Kontrollkopplung bedeutet, ein Flag zu übergeben, das dem anderen Modul sagt, was es tun soll. Stempelkopplung übergibt einen ganzen Datensatz, wenn nur ein Teil gebraucht wird. Datenkopplung, die loseste, übergibt nur einfache Daten, die alle gebraucht werden, über eine schmale, dokumentierte Schnittstelle. Kohäsion fragt, warum die Dinge in einem Modul zusammen sind. Am schwachen Ende steht die zufällige: ein utils-Modul mit Datumsformatierung und einem Mehrwertsteuersatz. Am starken Ende steht die funktionale: ein Steuermodul, das die Mehrwertsteuer berechnet, und nur das.

Die Stufen beschreiben die Art der Abhängigkeit. Architekturfachleute zählen sie außerdem. Afferente Kopplung ist die Anzahl der Module, die von einer Komponente abhängen: ihre eingehenden Pfeile. Efferente Kopplung ist die Anzahl derer, von denen sie abhängt: ihre ausgehenden Pfeile. Die Begriffe stammen aus der Anatomie, wo afferente Nerven Signale hinein- und efferente Nerven sie hinausleiten. Eine Komponente mit hoher afferenter Kopplung, etwa eine Geldbibliothek, die vierzig Module nutzen, ist verantwortlich. Jede Änderung kann viele andere brechen, also muss sie sich vorsichtig und selten ändern. Eine mit hoher efferenter Kopplung, etwa ein Checkout-Workflow, der fünf andere Module aufruft, ist exponiert. Problematisch wird es, wenn eine Komponente beides ist.

Zwischen Services kommen drei weitere Arten hinzu, und keine zeigt sich in einer Funktionssignatur. Zeitliche Kopplung bedeutet, dass ein Teil nur funktioniert, wenn ein anderer im selben Moment verfügbar ist. Wenn der Checkout die Zahlungen direkt aufruft und die Zahlungen ausgefallen sind, ist es auch der Checkout. Deployment-Kopplung bedeutet, dass zwei Teile gemeinsam ausgeliefert werden müssen. Wenn jedes Release des Bestellmoduls ein Release der Abrechnung braucht, sind sie in der Praxis eine Einheit, was auch immer das Diagramm sagt. Semantische Kopplung bedeutet, sich über eine Bedeutung einig zu sein, die keine Schnittstelle ausdrückt, etwa dass Status vier versandt bedeutet oder dass Preise in Pence angegeben sind. Solche Übereinkünfte brechen lautlos.

Hier ein ausgearbeitetes Refactoring. Vorher ruft das Bestellmodul eines Onlineshops bei jeder Bestellung direkt die Abrechnung auf und übergibt ein Modus-Flag. Das ist Kontrollkopplung und zugleich zeitliche Kopplung: Das Bestellmodul wartet auf die Abrechnung. Die Abrechnung liest außerdem die Bestelltabelle direkt und entschlüsselt deren Statuscodes und Mehrwertsteuerspalte. Das ist globale Kopplung und dazu semantische Kopplung. Nachher veröffentlicht das Bestellmodul über einen Message Broker ein Ereignis „Bestellung aufgegeben“, das genau die Fakten enthält, die die Abrechnung braucht, einschließlich der Summen, die das Bestellmodul bereits berechnet hat. Jeder Service besitzt seine eigenen Daten. Übrig bleibt Datenkopplung über einen kleinen, veröffentlichten Vertrag. Und das Bestellmodul wartet nicht mehr auf die Abrechnung.

Messen wir, was sich geändert hat. Vorher: eine gemeinsame Tabelle, ein Modus-Flag und versteckte Statuscodes. Nachher: ein versioniertes Ereignis mit fünf Feldern. Vorher scheiterte eine Bestellung, wenn die Abrechnung ausgefallen war. Jetzt wartet das Ereignis im Broker, bis die Abrechnung wieder läuft. Vorher erforderte eine Änderung an der Bestelltabelle ein gemeinsames Release. Jetzt kann das Bestellmodul sie frei ändern. Aber es gibt neue Kosten: einen Broker, der betrieben werden muss, Rechnungen erst einige Sekunden später und ein Ereignis, das jetzt ein öffentlicher Vertrag ist. Kopplung wurde nicht beseitigt, sie wurde gewählt. Sie ist jetzt explizit, schmal und versioniert.

Der vollständige Abschnitt enthält Tabellen zu jeder Stufe, ein ausgearbeitetes Beispiel zum Zählen von Abhängigkeiten und die Taktiken zum Lockern von Kopplung, jede mit ihrem Preis. Als Nächstes betrachten wir in Abschnitt drei Punkt drei Information Hiding. Bis gleich.

Was Sie wissen müssen

Zwei Maße für Struktur

Kopplung ist der Grad, in dem ein Modul von einem anderen abhängt, sodass eine Änderung am einen eine Änderung am anderen erzwingen kann. Kohäsion ist der Grad, in dem die Elemente innerhalb eines Moduls zusammengehören, weil sie einem einzigen Zweck dienen. Beide Ideen führte Larry Constantine Ende der 1960er-Jahre im Rahmen der Structured-Design-Bewegung ein; veröffentlicht wurden sie 1974 im Artikel Structured Design von Wayne Stevens, Glenford Myers und Constantine im IBM Systems Journal und dann ausführlicher in Edward Yourdons und Constantines Buch Structured Design (1979). Abschnitt 0.3 hat sie informell eingeführt; dieser Abschnitt macht sie präzise.

Beide hängen zusammen. Wenn die Dinge, die sich gemeinsam ändern, im selben Modul liegen (hohe Kohäsion), überqueren weniger Änderungen Modulgrenzen, und die Module müssen weniger voneinander wissen (geringe Kopplung). Yourdon und Constantine gingen so weit zu sagen, dass in der Praxis die Kohäsion die wichtigere der beiden sei. Die Änderbarkeitstaktiken in Abschnitt 2.6 (Kohäsion erhöhen, Kopplung verringern) beruhen genau auf diesem Paar.

Gleich zu Beginn eine Klarstellung: Kopplung ist kein Mangel. Ein System, dessen Module nie voneinander abhingen, würde nichts Nützliches tun. Martin Fowler warnte 2001 in einem Beitrag zu diesem Thema, dass der Versuch, Kopplung ganz zu beseitigen, alles nur in ein riesiges Modul schiebt, in dem die Kopplung verborgen statt verschwunden ist. Das Ziel ist, Kopplung bewusst zu wählen: wenige Abhängigkeiten, über schmale und stabile Schnittstellen, in sinnvolle Richtungen und so schwach, wie es die Aufgabe erlaubt.

Die klassischen Skalen

Die klassische Kopplungsskala von der engsten (Inhalt) zur losesten (Daten) und die Kohäsionsskala von der schwächsten (zufällig) zur stärksten (funktional)Kopplung zwischen Modulen: rechtes Ende anstrebenInhaltGlobalExternKontrolleStempelDatenenger, schlimmerloser, besserKohäsion in einem Modul: rechtes Ende anstrebenZufäl-ligLogischZeitlichProzeduralKommuni-kativSequenziellFunktionalschwächer/üblerstärker, besser
Die Skalen des Structured Design: Kopplung von Inhalt (am engsten) bis Daten (am losesten) und Kohäsion von zufällig (am schwächsten) bis funktional (am stärksten).

Die Stufen der Kopplung

Structured Design ordnete die Arten, wie zwei Module verbunden sein können, von der schädlichsten bis zur harmlosesten. Die unten gezeigte Form mit sechs Stufen wird meist auf Glenford Myers’ Bücher zum Composite Design Mitte der 1970er-Jahre zurückgeführt und später von Meilir Page-Jones bekannt gemacht; Namen und Abgrenzungen unterscheiden sich zwischen den Quellen leicht, aber dies ist die Form, die man in den meisten Lehrbüchern findet. Definiert wurden die Stufen für Prozeduren innerhalb eines Programms, doch jede hat eine direkte Entsprechung zwischen Komponenten und Services, und dort begegnen sie Architekturfachleuten heute.

Kopplungsstufen, die engste zuerst

NiveauWas sie bedeutetIm CodeZwischen Services
InhaltEin Modul greift in die Interna eines anderen ein: ändert dessen private Daten oder springt in dessen Code.cart._items von außen setzen; die Funktion eines anderen Moduls per Monkey-Patching ersetzen.Die Datenbanktabellen eines anderen Service direkt lesen oder schreiben.
GlobalMehrere Module lesen und schreiben dieselben globalen Daten.Ein globales config-Dictionary, das jede Funktion ändern darf.Mehrere Services teilen sich ein Datenbankschema oder einen gemeinsamen Cache-Schlüsselraum.
ExternModule teilen sich ein von außen vorgegebenes Format, Protokoll oder Gerät.Zwei Module parsen beide dasselbe Dateiformat eines Herstellers.Mehrere Services hängen vom Dateilayout eines Partners oder von einer bestimmten Version eines Nachrichtenprotokolls ab.
KontrolleEin Modul übergibt ein Flag, das dem anderen sagt, was es tun soll; der Aufrufer kennt also die interne Logik des Aufgerufenen.process(order, mode="refund")Eine API mit einem action-Feld, das völlig unterschiedliche Verhalten auswählt.
StempelEin Modul erhält eine ganze Datenstruktur, nutzt aber nur einen Teil davon.Das vollständige Customer-Objekt an eine Funktion übergeben, die nur die E-Mail-Adresse braucht.Den gesamten Kundendatensatz in jedem Ereignis senden, obwohl die Consumer zwei Felder brauchen.
DatenModule kommunizieren nur über Parameter mit einfachen Daten, die alle benötigt werden.send_receipt(email, order_id, total)Eine schmale, dokumentierte API oder ein Ereignis mit nur den Feldern, die die Consumer brauchen.

Zwei Warnungen. Erstens ist die Skala ein Wegweiser für Risiken, kein Gesetz. Stempelkopplung ist oft eine vernünftige Abwägung: Ein ganzes Objekt zu übergeben ist einfacher, als sechs Felder auszupacken, und der Preis ist nur, dass der Empfänger jetzt brechen kann, wenn Felder geändert werden, die er gar nicht nutzt. Zweitens sagt die Skala nichts darüber, wie viele Abhängigkeiten es gibt oder wie oft sich die andere Seite ändert. Ein einzelner kontrollgekoppelter Aufruf an ein Modul, das sich nie ändert, kann harmlos sein; Datenkopplung an ein Modul, das sich wöchentlich ändert, kann schmerzen. Später in diesem Kapitel bietet Connascence ein feineres Vokabular, und Vlad Khononovs Balancing Coupling in Software Design (2024) argumentiert, dass die Stärke der Kopplung immer zusammen mit der Distanz zwischen den Modulen und ihrer Volatilität beurteilt werden sollte.

# Control coupling: the caller chooses the callee's internal branch
def handle_payment(order, mode):
    if mode == "charge":
        ...  # call the provider, store the transaction
    elif mode == "refund":
        ...  # look up the transaction, reverse it
    elif mode == "dry_run":
        ...  # validate only

handle_payment(order, "refund")

# Data coupling: separate operations, each taking only what it needs
def charge(order_id: str, amount_pence: int) -> str: ...
def refund(transaction_id: str, amount_pence: int) -> None: ...

refund("tx_81f2", 1999)
Kontrollkopplung (oben) im Vergleich zu Datenkopplung (unten): Das Flag zwingt den Aufrufer zu wissen, wie der Aufgerufene intern organisiert ist.

Die Stufen der Kohäsion

Kohäsion fragt, warum die Elemente eines Moduls zusammen sind. Yourdon und Constantine beschrieben sieben Arten der Zusammengehörigkeit, wieder von der schlechtesten zur besten. Die unteren Stufen gruppieren Dinge aus Gründen, die nichts mit dem Problem zu tun haben (sie wurden zufällig zur selben Zeit geschrieben oder laufen im selben Moment); die oberen Stufen gruppieren Dinge, weil sie mit denselben Daten oder auf denselben einzelnen Zweck hin arbeiten. Spätere Lehrbuchzusammenfassungen sind sich in der groben Rangfolge einig: Zufällige und logische Kohäsion sind schlecht, kommunikative und sequenzielle sind gut und funktionale ist am besten, wobei die Abstände zwischen den Stufen nicht gleichmäßig sind.

Kohäsionsstufen, die schwächste zuerst

NiveauElemente sind gruppiert, weil …Beispiel
Zufällig… es überhaupt keine sinnvolle Beziehung gibt.Ein utils-Modul mit Datumsformatierung, einem Retry-Decorator und einem Mehrwertsteuersatz.
Logisch… sie die gleiche Art von Aufgabe erledigen, ausgewählt über ein Flag.Ein exports-Modul mit einer Funktion, die je nach Parameter CSV, PDF oder XML schreibt.
Zeitlich… sie zur selben Zeit laufen.Eine startup()-Routine, die die Datenbank öffnet, Caches vorwärmt und einen Monitoring-Ping sendet.
Prozedural… sie in einer Ablauffolge aufeinanderfolgen, allerdings mit unterschiedlichen Daten.Eine Funktion, die ein Formular validiert, dann die IP-Adresse des Benutzers protokolliert und dann ein Menü aktualisiert.
Kommunikativ… sie mit denselben Daten arbeiten.Ein Modul, das mehrere Berichte aus derselben Bestellhistorie berechnet.
Sequenziell… die Ausgabe des einen die Eingabe des nächsten ist.Einen Kontoauszug parsen, dann seine Posten kategorisieren, dann jede Kategorie aufsummieren.
Funktional… sie zusammen eine klar definierte Aufgabe erfüllen und sonst nichts.Ein Tax-Modul, das die Mehrwertsteuer für einen Warenkorb berechnet, und nur das.

Auf Architekturebene ist Kohäsion die Frage, ob eine Komponente oder ein Service einen einzigen Daseinsgrund hat. Ein Service namens CustomerService, der Anmeldung, Treuepunkte und Marketingeinstellungen verwaltet, ist bestenfalls logisch kohäsiv: Drei Teams werden ihn aus drei voneinander unabhängigen Gründen ändern. Eine gute Faustregel ist, den Zweck eines Moduls in einem Satz zu beschreiben, ohne und zu verwenden; Abschnitt 3.5 entwickelt das zum Single Responsibility Principle weiter, das über die Personen definiert ist, die Änderungen anfordern.

Afferente und efferente Kopplung

Die obigen Stufen beschreiben die Art einer Abhängigkeit. Architekturfachleute müssen Abhängigkeiten außerdem zählen. Afferente Kopplung (Ca) ist die Anzahl der Module außerhalb einer Komponente, die von ihr abhängen: ihre eingehenden Pfeile. Efferente Kopplung (Ce) ist die Anzahl der Module außerhalb der Komponente, von denen sie abhängt: ihre ausgehenden Pfeile. Die Begriffe stammen aus der Anatomie, wo afferente Nerven Signale hinein- und efferente Nerven sie hinausleiten. Robert C. Martin definierte sie 1994 für Pakete, indem er Klassen zählte; heutige Werkzeuge zählen Klassen, Module oder Pakete, prüfen Sie also immer, was ein Werkzeug zählt, bevor Sie Zahlen vergleichen.

Die beiden Zahlen erzählen unterschiedliche Geschichten. Eine Komponente mit hohem Ca, etwa eine gemeinsame money-Bibliothek, die von vierzig Modulen genutzt wird, ist verantwortlich: Jede Änderung an ihr kann viele andere brechen, also muss sie sich vorsichtig und selten ändern. Eine Komponente mit hohem Ce, etwa ein Checkout-Workflow, der Preisgestaltung, Lagerbestand, Zahlung, Lieferung und Benachrichtigungen aufruft, ist exponiert: Sie muss sich unter Umständen immer dann ändern, wenn sich eine davon ändert. Keins von beiden ist an sich falsch; das Problem entsteht, wenn eine Komponente stark von anderen genutzt wird und selbst von vielen volatilen Dingen abhängt. Abschnitt 3.4 macht aus Ca und Ce Martins Instabilitätsmetrik und die darauf aufbauenden Regeln.

Ausgearbeitetes Beispiel

Ausgearbeitetes Beispiel: Ca und Ce in einer Essensliefer-App zählen

Sieben Module (Checkout, Speisekarte, Preisgestaltung, Zahlungen, Benachrichtigungen, Kurierverfolgung und Admin) haben folgende Abhängigkeiten („A hängt von B ab“ wird als A zu B geschrieben): Checkout zu Speisekarte, Checkout zu Preisgestaltung, Checkout zu Zahlungen, Checkout zu Benachrichtigungen; Kurierverfolgung zu Benachrichtigungen; Preisgestaltung zu Speisekarte; Admin zu Speisekarte, Admin zu Preisgestaltung.

SpeisekarteCa = 3 (Checkout, Preisgestaltung, Admin); Ce = 0
PreisgestaltungCa = 2 (Checkout, Admin); Ce = 1 (Speisekarte)
BenachrichtigungenCa = 2 (Checkout, Kurierverfolgung); Ce = 0
CheckoutCa = 0; Ce = 4 (Speisekarte, Preisgestaltung, Zahlungen, Benachrichtigungen)
LesenVon Speisekarte hängen die meisten Module ab, also braucht ihre Schnittstelle die meiste Sorgfalt und die langsamsten Änderungen. Checkout hängt von den meisten ab, sollte also am leichtesten zu ändern sein und Änderungen anderer auffangen.

Das Zählen dauert mit einem Abhängigkeitswerkzeug Minuten und macht aus einem vagen Gefühl („Speisekarte ist zentral“) eine Zahl, die Sie im Zeitverlauf verfolgen können.

Kopplung in verteilten Systemen

Wenn Module zu Services werden, die über ein Netzwerk kommunizieren, gelten die klassischen Stufen weiter, aber drei weitere Arten von Kopplung treten in den Vordergrund. Keine davon ist in einer Funktionssignatur sichtbar, weshalb Teams, die ein System in Services aufteilen, oft überrascht sind, wie stark gekoppelt die Services noch sind.

Zeitliche Kopplung bedeutet, dass ein Teil nur funktioniert, wenn ein anderer im selben Moment verfügbar ist und antwortet. Ein synchroner HTTP-Aufruf von Checkout an Zahlungen koppelt ihre Verfügbarkeit: Fällt Zahlungen aus, fällt auch der Checkout aus, und die gemeinsame Verfügbarkeit ist das Produkt der beiden (Abschnitt 2.5). Asynchrone Nachrichten über eine Queue beseitigen das, um den Preis von letztlich statt sofort vorliegenden Ergebnissen. (Im Design auf Code-Ebene bedeutet derselbe Begriff manchmal, dass Aufrufe in einer festen Reihenfolge erfolgen müssen, etwa open() vor read(); der Kontext macht die Bedeutung meist klar.)

Deployment-Kopplung bedeutet, dass zwei Teile gemeinsam oder in einer festen Reihenfolge ausgeliefert werden müssen, weil eine Änderung am einen den anderen bricht. Wenn jedes Release des Bestellmoduls ein gleichzeitiges Release der Abrechnung braucht, sind die beiden in der Praxis eine deploybare Einheit, was auch immer das Diagramm sagt. Abwärtskompatible Änderungen, versionierte APIs und das Expand-and-Contract-Muster sind die üblichen Heilmittel.

Semantische Kopplung bedeutet, dass sich zwei Teile über eine Bedeutung einig sein müssen, die keine Schnittstelle ausdrückt: dass eine Bestellung mit status = 4 versandt ist, dass Preise in Pence angegeben sind und die Mehrwertsteuer enthalten, dass ein „aktiver Kunde“ in den letzten 90 Tagen bestellt hat. Solche Übereinkünfte brechen lautlos, wenn eine Seite ihre Auslegung ändert. Das Heilmittel ist, die Bedeutung im Vertrag explizit zu machen (benannte Zustände, Einheiten in Feldnamen, veröffentlichte Schemas) oder die Regel an einer Stelle zu halten, die andere fragen.

Taktiken zur Verringerung von Kopplung

Schmale Schnittstellen. Legen Sie nur die wenigsten Operationen und Felder offen, die Clients brauchen. Jedes zusätzliche Feld in einer API-Antwort ist etwas, von dem ein Client abhängig werden kann (Abschnitt 3.3 zu Hyrums Gesetz). Vermittler. Setzen Sie etwas zwischen die beiden Parteien, damit keine die Details der anderen kennt: einen Adapter oder Anti-Corruption Layer, der das Modell eines Lieferanten übersetzt, ein API-Gateway oder einen Message Broker, der Sender und Empfänger zeitlich und in ihrer Identität entkoppelt. Ereignisse statt Befehle. Ein Befehl (Abrechnung, erstelle eine Rechnung) zwingt den Sender, den Empfänger und dessen Aufgabe zu kennen; ein Ereignis (eine Bestellung wurde aufgegeben) lässt den Sender eine Tatsache feststellen und beliebig viele Empfänger reagieren, sodass sich neue Consumer hinzufügen lassen, ohne den Producer zu ändern. Datenhoheit. Jedes Datum hat einen Eigentümer, der es schreibt; andere Module fragen den Eigentümer über seine Schnittstelle oder halten eine eigene, über Ereignisse gespeiste Kopie und lesen nie die Tabellen des Eigentümers (Abschnitt 7.1).

Jede Taktik hat ihren Preis. Vermittler fügen einen Sprung hinzu und etwas, das zusätzlich betrieben werden muss. Ereignisse geben die sofortige Konsistenz auf und machen den Ablauf eines Geschäftsprozesses schwerer an einer Stelle zu überblicken. Kopien von Daten können veraltet sein. Datenhoheit bedeutet, APIs zu bauen und zu versionieren, die eine gemeinsame Tabelle umsonst geliefert hat. Die Abwägung lohnt sich dort, wo Änderungen häufig oder Teams getrennt sind, und oft nicht zwischen zwei Klassen, die eine einzige Person pflegt.

Ausgearbeitetes Beispiel: ein eng gekoppeltes Paar aus Bestellung und Abrechnung refaktorisieren

Das Modul Bestellungen eines Onlineshops ruft bei jeder aufgegebenen Bestellung direkt das Modul Abrechnung auf. Die Abrechnung liest dann die Bestelltabelle, um die Details zu ermitteln, interpretiert die numerischen Codes der Statusspalte und berechnet die Mehrwertsteuer aus den Positionspreisen neu. Der folgende Code ist eine vereinfachte Fassung; er zeigt drei der Probleme auf einmal.

# orders.py
def place_order(basket, customer_id):
    order_id = db.insert("orders", customer_id=customer_id, status=1,
                         lines=basket.lines)
    billing.process(order_id, mode="invoice")   # Orders waits for Billing
    return order_id

# billing.py
def process(order_id, mode):
    row = db.query_one("SELECT * FROM orders WHERE id = %s", order_id)
    if row["status"] != 1:              # 1 means 'placed', by convention
        return
    net = sum(l["price"] * l["qty"] for l in row["lines"])
    vat = round(net * 0.2)               # VAT rule duplicated here
    if mode == "invoice":
        db.insert("invoices", order_id=order_id, net=net, vat=vat)
    elif mode == "credit_note":
        db.insert("credit_notes", order_id=order_id, net=-net, vat=-vat)
Vorher: Kontrollkopplung (das Modus-Flag), globale Kopplung (beide Module nutzen die Bestelltabelle) und semantische Kopplung (Statuscodes und Mehrwertsteuerregeln sind beiden bekannt).

Jede Änderung am Aufbau der Bestelltabelle, an den Statuscodes oder an den Mehrwertsteuerregeln muss nun in beiden Modulen gleichzeitig erfolgen. Bestellungen kann keine Bestellung annehmen, wenn die Abrechnung langsam ist oder ausfällt. Und die beiden Verhalten der Abrechnung werden über ein Flag ausgewählt, das Bestellungen kennen muss. Das Refactoring verläuft in vier Schritten: Bestellungen ruft die Abrechnung nicht mehr auf, sondern veröffentlicht ein OrderPlaced-Ereignis, das genau die Fakten enthält, die die Abrechnung braucht, einschließlich der Summen, die Bestellungen bereits berechnet hat; die Abrechnung abonniert es und erfasst Rechnungen in einem eigenen Speicher; die beiden Verhalten werden zu zwei Operationen; und die Bestelltabelle wird privat für Bestellungen.

# orders.py
def place_order(basket, customer_id):
    order = Order.create(customer_id, basket)      # computes net and VAT once
    repository.save(order)
    events.publish("OrderPlaced", {
        "schema_version": 1, "order_id": order.id,
        "customer_id": customer_id,
        "net_pence": order.net_pence, "vat_pence": order.vat_pence})
    return order.id

# billing.py
@events.subscribe("OrderPlaced")
def on_order_placed(event):
    create_invoice(event["order_id"], event["net_pence"], event["vat_pence"])

def create_invoice(order_id, net_pence, vat_pence): ...
def create_credit_note(order_id, net_pence, vat_pence): ...
Nachher: Bestellungen stellt eine Tatsache über ein versioniertes Ereignis fest; die Abrechnung besitzt ihre eigenen Daten und kennt weder die Tabellen noch die Codes oder Steuerregeln von Bestellungen.

Das Paar aus Bestellung und Abrechnung vorher und nachher

Vorher: Abrechnung liest die DB von Bestellungen, Aufruf mit Modus-Flag. Nachher: Bestellungen veröffentlicht ein OrderPlaced-Event, Abrechnung hält eigene DatenVorherBestell.Abrechn.Modus-FlagBestelltabelle(gehört Bestell.)liest Zeilen,Statuscodes,MwSt.-SpalteGlobale Kopplung über die Tabelle,Kontrollkopplung über das Flag,zeitl. Kopplung: Bestell. wartetbei jeder BestellungNachherBestell.Abrechn.MessageBrokerOrderPlaced-Event(IDs, Summen, Version)Bestell-DBnur Bestell.Rechnungs-DBnur Abrechn.Datenkopplung über einen veröff.Vertrag; jeder Service besitzt s. Daten;Bestell. wartet nicht auf Abrechnung
Das Refactoring ersetzt einen direkten Aufruf mit Flag und eine gemeinsame Tabelle durch ein veröffentlichtes Ereignis und getrennte Datenspeicher.

Ausgearbeitetes Beispiel

Was sich geändert hat, gemessen

Ein Vergleich der beiden Versionen anhand des Kopplungsvokabulars aus diesem Abschnitt.

Art der KopplungVorher: inhaltsähnliche und globale Kopplung (gemeinsame Tabelle), Kontrollkopplung (Modus-Flag), semantische Kopplung (Status 1, Mehrwertsteuerregel). Nachher: Datenkopplung über ein versioniertes Ereignis mit fünf Feldern.
Efferente Kopplung von BestellungenVorher: Ce umfasst die Abrechnung. Nachher: Bestellungen hängt nur vom Ereignisvertrag und vom Broker ab; es weiß nicht, dass die Abrechnung existiert.
Zeitliche KopplungVorher: Eine Bestellung scheitert, wenn die Abrechnung ausgefallen ist. Nachher: Das Ereignis wartet im Broker, bis die Abrechnung wieder läuft.
Deployment-KopplungVorher: Eine Änderung an der Bestelltabelle erfordert ein gemeinsames Release. Nachher: Bestellungen kann seine Tabelle frei ändern; nur Änderungen am Ereignis erfordern Sorgfalt, und die Versionierung fängt sie ab.
Neue KostenEin Broker, der betrieben werden muss; Rechnungen erscheinen Sekunden später statt sofort; das Ereignisschema ist jetzt ein öffentlicher Vertrag, der versioniert werden muss; der Prozess ist ohne gute Observability schwerer nachzuverfolgen.

Das Ereignis enthält die Summen, die Bestellungen berechnet hat, sodass die Mehrwertsteuer an einer Stelle berechnet wird. Die Abrechnung muss weder die Steuerregeln noch den Aufbau der Bestellpositionen kennen.

Das Refactoring hat eine Wahl getroffen, nicht alle Kopplung beseitigt: Die Abrechnung hängt weiterhin von der Bedeutung von OrderPlaced ab. Diese Abhängigkeit ist jetzt explizit, schmal und versioniert, und genau das bedeutet lose Kopplung in der Praxis.

Wie Fachleute diese Ideen nutzen

Erfahrene Designer nennen die Namen der Stufen in Reviews selten, wenden sie aber ständig an: Sie hinterfragen jeden Flag-Parameter, suchen nach gemeinsamen Tabellen und globalem Zustand, fragen nach dem einen Zweck jedes Moduls und beobachten, welche Module sich in der Versionskontrolle gemeinsam ändern. An den Grenzen zwischen Teams und Services sind sie streng, weil Kopplung dort teuer aufzulösen ist; innerhalb eines kleinen Moduls, das einer Person gehört, sind sie gelassen, weil sie dort billig ist.

Außerdem machen sie Kopplung sichtbar. Ein aus dem Code erzeugter Abhängigkeitsgraph, eine Zählung von Ca und Ce pro Komponente und eine Liste der Datenbanktabellen, auf die mehr als ein Service zugreift, gehören zu den aufschlussreichsten Sichten auf eine Architektur, und alle lassen sich automatisch erzeugen und im Build verfolgen. Der nächste Abschnitt wendet sich dem Prinzip zu, das Kopplung von vornherein schwach hält: Entscheidungen hinter Schnittstellen verbergen.

Prüfen Sie Ihr Verständnis

Warum ist eine Funktion mit einem Modus-Parameter, der völlig unterschiedliche Verhalten auswählt, ein Beispiel für Kontrollkopplung?

Weil der Aufrufer wissen muss, wie der Aufgerufene intern organisiert ist, und dessen internen Zweig auswählt; eine Änderung der Logik des Aufgerufenen kann Änderungen bei den Aufrufern erzwingen.

Ein Modul hat Ca = 12 und Ce = 1. Was deutet das an?

Viele Module hängen von ihm ab, und es selbst hängt von wenig ab; Änderungen daran sind also riskant und sollten selten und vorsichtig erfolgen, und seine Schnittstelle verdient besondere Stabilität.

Zwei Services kommunizieren nur über Ereignisse, müssen aber immer gemeinsam ausgeliefert werden. Welche Art von Kopplung bleibt bestehen?

Deployment-Kopplung, meist verursacht durch nicht abwärtskompatible Änderungen am gemeinsamen Ereignis oder durch verborgene semantische Übereinkünfte.

Nennen Sie einen Preis dafür, einen direkten Aufruf durch ein Ereignis zu ersetzen.

Ergebnisse werden letztlich statt sofort konsistent; außerdem bekommen Sie einen Broker, der betrieben werden muss, ein Ereignisschema, das versioniert werden muss, und einen Ablauf, der schwerer nachzuverfolgen ist.

Weiter · 3.3Information HidingWarum Module Entscheidungen verbergen sollten, wie sich Information Hiding von Kapselung unterscheidet, Schnittstellen als Verträge und Design by Contract, Leaky Abstractions, tiefe Module, Hyrums Gesetz und wie man APIs zwischen Komponenten stabil hält.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: „Unsere beiden Services teilen sich eine Datenbanktabelle. Wie trenne ich sie Schritt für Schritt ohne eine Big-Bang-Migration?“