Softwarearchitektur verstehen und entscheiden

Aaron KreisÜber mich
Grundlagen

Loose Coupling and High Cohesion

Wenig Abhängigkeiten nach außen, starker Zusammenhalt innen

Die Idee und ihr Nutzen

Eine Versicherung erfasst einen Schaden, bewertet ihn und weist danach eine Zahlung an. Erfassen und Bewerten erledigt ein Programm namens Schadendienst, die Auszahlung ein zweites Programm, der Zahlungsdienst. Alles, was zur Bewertung eines Schadens gehört, steht nur im Schadendienst, auch die Regel zur Selbstbeteiligung. Der Schadendienst fordert eine Zahlung über den vereinbarten Aufruf „Zahlung anweisen“ an und übergibt dabei nur Schadennummer, Betrag und Empfänger. Wie der Zahlungsdienst die Zahlung ausführt, muss der Schadendienst nicht wissen. So betrifft eine Änderung möglichst nur einen der beiden Dienste: Entfällt die Selbstbeteiligung, ändern die Entwickler nur den Schadendienst. Dass alles zur Bewertung eines Schadens im Schadendienst beisammen liegt, nennt man High Cohesion, auf Deutsch hohe Kohäsion, also hoher Zusammenhalt. Dass der Schadendienst vom Zahlungsdienst nur den Aufruf „Zahlung anweisen“ kennt, nennt man Loose Coupling, auf Deutsch lose Kopplung.

Bausteine: Erfassen; Bewerten: kennt die Regel zur Selbstbeteiligung; Zahlungsdienst: zahlt übergebene Beträge aus, kennt keine Bewertungsregeln. Verbindungen: Bewerten — Zahlungsdienst (Zahlung anweisen: Schadennummer, Betrag, Empfänger). Alles zur Bewertung steht im Schadendienst; mit dem Zahlungsdienst ist der Schadendienst nur über eine einzige vereinbarte Schnittstelle verbunden.

Wie funktioniert es?

Ein Kunde meldet dem Schadendienst einen Glasschaden über 800 Euro. Der Schadendienst erfasst den Schaden und bewertet ihn. Dabei wendet der Schadendienst die Regel zur Selbstbeteiligung an, die nur in ihm steht: Bei Glasschäden beträgt die Selbstbeteiligung 150 Euro, und der Schadendienst gibt 650 Euro frei. Erst dann fordert der Schadendienst die Zahlung beim Zahlungsdienst mit dem vereinbarten Aufruf „Zahlung anweisen“ an und übergibt Schadennummer, Betrag und Empfänger. Mehr erfährt der Zahlungsdienst nicht. Der Zahlungsdienst zahlt die 650 Euro aus und meldet „ausgezahlt“ zurück, ohne die Regel zur Selbstbeteiligung zu kennen. Wie der Zahlungsdienst die Zahlung technisch ausführt, erfährt der Schadendienst nicht. Ein Jahr später entfällt die Selbstbeteiligung bei Glasschäden. Das Team Schaden, das den Schadendienst entwickelt, ändert die Regel im Schadendienst, und der Zahlungsdienst bleibt unberührt. Umgekehrt gilt dasselbe für das Team Zahlung, das den Zahlungsdienst entwickelt: Für Echtzeitüberweisungen ändert das Team Zahlung nur den Zahlungsdienst, solange der Aufruf „Zahlung anweisen“ dafür ausreicht.

Die vereinbarte Form eines Aufrufs heißt Schnittstelle. Sie besteht aus dem Namen des Aufrufs, den übergebenen Daten und der Antwort. Die Schnittstelle „Zahlung anweisen“ haben das Team Schaden und das Team Zahlung vereinbart. Der Zahlungsdienst bietet sie an, und der Schadendienst ruft sie auf. Lose Kopplung und hohe Kohäsion bilden zusammen einen Grundsatz für den Zuschnitt von Software, also für ihre Aufteilung in Teile. Sie sind kein Bauteil und kein Produkt. Lose Kopplung heißt: Programmteile hängen möglichst wenig voneinander ab. Die Programmteile kennen voneinander nur eine kleine, beständige Schnittstelle, nicht den Code und die Datenbanktabellen des anderen Programmteils. Ändert sich der Code oder eine Tabelle eines Programmteils, sind die anderen Programmteile davon nicht betroffen. Hohe Kohäsion heißt: Was fachlich zusammengehört, liegt im selben Programmteil. Der Schadendienst enthält zum Beispiel alle Regeln, um Schäden zu erfassen und zu bewerten, aber keine Regeln zur Auszahlung. Beide Eigenschaften hängen zusammen. Steht die Regel zur Selbstbeteiligung nur im Schadendienst, braucht der Zahlungsdienst weder Schadenart noch Schadenbetrag. Stünde die Regel auch im Zahlungsdienst, müsste der Schadendienst dem Zahlungsdienst diese Angaben zusätzlich übergeben. Kopplung zeigt sich in verschiedenen Formen:

  • Zwei Programmteile nutzen dieselben Datenbanktabellen oder dieselben Datenformate.
  • Ein Programmteil ruft einen anderen direkt auf und muss auf dessen Antwort warten.
  • Ein Programmteil ist darauf angewiesen, dass ein anderer intern auf eine bestimmte Weise abläuft.

Ganz vermeiden lässt sich Kopplung nie, denn Programmteile, die zusammenarbeiten, müssen etwas voneinander wissen. Das Ziel ist, die Kopplung auf eine beschriebene Schnittstelle zu beschränken, so wie der Schadendienst vom Zahlungsdienst nur den Aufruf „Zahlung anweisen“ kennt.

Der Grundsatz garantiert nicht, dass jede Änderung nur einen Programmteil betrifft. Reicht die Schnittstelle „Zahlung anweisen“ für eine Änderung nicht mehr aus, müssen das Team Schaden und das Team Zahlung die Schnittstelle gemeinsam ändern. Der Schadendienst ruft den Zahlungsdienst über das Netz auf. Dabei kann die Antwort des Zahlungsdienstes ausbleiben. Eine fehlende Antwort heißt nicht, dass nicht gezahlt wurde. Der Schadendienst darf die Zahlung deshalb nur so wiederholen, dass der Zahlungsdienst eine doppelte Anweisung erkennt. Eine doppelte Anweisung kann der Zahlungsdienst zum Beispiel an der Schadennummer erkennen.

Lose Kopplung, hohe Kohäsion: Selbstbeteiligung nur im Schadendienst

Beteiligte: Kunde: meldet einen Glasschaden über 800 €; Schadendienst: erfasst und bewertet; kennt die Selbstbeteiligung; Zahlungsdienst: zahlt übergebene Beträge aus, kennt keine Bewertungsregeln; Team Schaden: ändert Bewertungsregeln; Team Zahlung: ändert Zahlungsarten. Ablauf: 1. Ein Kunde meldet dem Schadendienst einen Glasschaden über 800 Euro. 2. Der Schadendienst bewertet den Schaden mit der Regel zur Selbstbeteiligung, die nur in ihm steht, und gibt 650 Euro frei. 3. Über die einzige vereinbarte Schnittstelle „Zahlung anweisen“ übergibt der Schadendienst dem Zahlungsdienst Schadennummer, Betrag und Empfänger. 4. Der Zahlungsdienst zahlt die 650 Euro aus und meldet „ausgezahlt“ zurück, ohne die Regel zur Selbstbeteiligung zu kennen. 5. Ein Jahr später entfällt die Selbstbeteiligung bei Glasschäden. Das Team Schaden ändert die Regel im Schadendienst, der Zahlungsdienst bleibt unberührt. 6. Das Team Zahlung führt Echtzeitüberweisungen ein und ändert dafür nur den Zahlungsdienst, solange die Schnittstelle „Zahlung anweisen“ dafür ausreicht.

Wann ist es sinnvoll und wann nicht?

Lose Kopplung und hohe Kohäsion helfen

  • Funktionen, Programmteile, Dienste und ganze Systeme werden zugeschnitten. Der Grundsatz gilt auf jeder dieser Ebenen und für jedes System, das aus mehreren Programmteilen besteht. Ohne ihn entstehen Grenzen, über die eine einzelne fachliche Änderung wie der Wegfall der Selbstbeteiligung mehrere Teile betrifft.
  • Verschiedene Teams verantworten verschiedene Programmteile über längere Zeit. Jedes Team will seinen Programmteil ändern und ausliefern, ohne jedes Mal das andere Team einzubeziehen. Zwischen den Programmteilen verschiedener Teams erfordert jede Abhängigkeit Abstimmung. Weil der Schadendienst über die Schnittstelle nur Schadennummer, Betrag und Empfänger übergibt, kann das Team Zahlung Echtzeitüberweisungen einführen, ohne sich mit dem Team Schaden abzustimmen.
  • Dieselbe fachliche Regel steht bisher in mehreren Programmteilen. In einem über Jahre gewachsenen System steht die Regel zur Selbstbeteiligung zum Beispiel in der Erfassungsmaske, im Schadendienst und im Zahlungsdienst, der vor jeder Auszahlung noch einmal selbst nachrechnet. Soll die Selbstbeteiligung bei Glasschäden entfallen, müssen die Entwickler die Regel in allen drei Programmteilen gleich ändern, und immer wieder wird einer vergessen. Mit hoher Kohäsion steht die Regel nur im Schadendienst, und die Entwickler ändern sie nur dort.
  • Der Schadendienst kennt bisher Einzelheiten des Zahlungsdienstes, die der Schadendienst nicht braucht. Liest der Schadendienst zum Beispiel direkt aus den Datenbanktabellen des Zahlungsdienstes, muss das Team Schaden bei jeder Änderung an diesen Tabellen mitarbeiten, und beide Teams müssen ihre Auslieferungen aufeinander abstimmen. Kennt der Schadendienst die Tabellen nicht, funktioniert er nach einer solchen Änderung weiter.

Lose Kopplung und hohe Kohäsion helfen nicht

  • Zwei Programmteile gehören fachlich eng zusammen und ändern sich fast immer gemeinsam, etwa die Bewertung eines Schadens und die Regel zur Selbstbeteiligung. Lose Kopplung zwischen den beiden bringt nichts, denn jede Änderung an der Selbstbeteiligung betrifft trotzdem beide Programmteile und zusätzlich die Schnittstelle, über die sie Angaben austauschen. Nach dem Grundsatz der hohen Kohäsion legen die Entwickler die beiden zu einem Programmteil zusammen, so wie Bewertung und Selbstbeteiligung im Schadendienst. Die Schnittstelle entfällt und muss nicht mehr entworfen, beschrieben und bei jeder Änderung abgestimmt werden.
  • Ein kurzlebiger Prototyp soll nur eine Idee erproben und danach verworfen werden. Dann schreiben die Entwickler den Prototyp ohne sorgfältig entworfene Schnittstellen. Sie sparen den Aufwand, Schnittstellen zu entwerfen und zu beschreiben, für Code, der ohnehin weggeworfen wird.
  • Ein kleines Programm wird nur von einem einzigen Team gepflegt und als Ganzes ausgeliefert. Liest ein Programmteil direkt die Daten eines anderen Programmteils, muss das Team bei einer Änderung dieser Daten beide Programmteile ändern. Abstimmen muss es sich dafür mit keinem anderen Team, und es liefert beide Programmteile gemeinsam aus. Dann lässt das Team solche direkten Zugriffe zu und spart den Aufwand, Schnittstellen zu entwerfen, zu beschreiben und in neuen Fassungen anzubieten.

Nachteile und typische Fehler

Schnittstellen zwischen lose gekoppelten Programmteilen erfordern Aufwand. Sie müssen entworfen, beschrieben und bei Änderungen versioniert werden, also als neue Fassung neben der alten angeboten werden. Der richtige Zuschnitt ist nicht immer offensichtlich. Trennt eine Grenze Regeln, die fachlich zusammengehören, müssen die getrennten Programmteile laufend Angaben austauschen, etwa Schadenart und Schadenbetrag für die Selbstbeteiligung. Wer eine Grenze später verschiebt, muss alle Programmteile ändern, die die Schnittstelle schon nutzen. Laufen die Programmteile als getrennte Dienste und rufen einander über das Netz auf, kommen neue Probleme hinzu: Einzelne Dienste fallen aus, Antworten dauern zu lange, und Daten liegen in mehreren Diensten. Lose Kopplung innerhalb eines einzigen Programms vermeidet diese Probleme. Innerhalb eines Programms kann ein Programmteil aber jederzeit direkt die Tabellen oder den Code eines anderen Programmteils nutzen. Deshalb prüfen automatische Tests, ob jeder Programmteil die anderen nur über ihre Schnittstellen aufruft.

Typische Fehler und woran man sie erkennt:

  • Verteilte Regel: Die Regel zur Selbstbeteiligung steht im Schadendienst und noch einmal im Zahlungsdienst. Der Schadendienst übergibt dem Zahlungsdienst dafür zusätzlich Schadenart und Schadenbetrag, und der Zahlungsdienst rechnet vor jeder Auszahlung selbst nach. Entfällt die Selbstbeteiligung bei Glasschäden, ändert das Team Schaden die Regel im Schadendienst. Der Schadendienst gibt für den Glasschaden über 800 Euro nun 800 Euro frei. Der Zahlungsdienst rechnet weiter 800 Euro minus 150 Euro Selbstbeteiligung und lehnt die Zahlung über 800 Euro ab, weil der Betrag nicht zu seinen 650 Euro passt. Erkennbar ist das an abgelehnten Zahlungen kurz nach einer Regeländerung.
  • Heimliche Kopplung über Tabellen: Der Schadendienst liest direkt aus einer Datenbanktabelle des Zahlungsdienstes, ob ein Schaden schon ausgezahlt ist. Für die Echtzeitüberweisungen benennt das Team Zahlung eine Spalte dieser Tabelle um. Der Schadendienst findet die Spalte danach nicht mehr und meldet bei jeder Abfrage einen Fehler. Erkennbar ist das an Fehlern im Schadendienst nach einer Auslieferung des Zahlungsdienstes, obwohl sich die Schnittstelle nicht geändert hat.
  • Zu fein geschnitten: Erfassung, Bewertung und Selbstbeteiligung liegen in drei getrennten Diensten. Soll die Selbstbeteiligung künftig vom Tarif des Kunden abhängen, braucht der Dienst für die Selbstbeteiligung den Tarif. Den Tarif kennt aber nur der Dienst für die Erfassung. Die Teams müssen deshalb die Schnittstelle von der Erfassung zur Bewertung und die Schnittstelle von der Bewertung zur Selbstbeteiligung ändern und alle drei Dienste gemeinsam ausliefern. Erkennbar ist das, wenn kleine fachliche Änderungen regelmäßig mehrere Teams und Auslieferungen brauchen.
  • Zu breite Schnittstelle: Die Schnittstelle „Zahlung anweisen“ übergibt den ganzen Schadendatensatz statt Schadennummer, Betrag und Empfänger. Das Team Zahlung nutzt daraus zum Beispiel das Feld Schadenart, um Glasschäden getrennt zu verbuchen. Später teilt das Team Schaden das Feld Schadenart in zwei Felder auf. Der Zahlungsdienst findet das Feld Schadenart danach nicht mehr, und das Team Zahlung muss den Zahlungsdienst zur selben Zeit ändern. Erkennbar ist das an Feldern, die der Zahlungsdienst liest, obwohl sie fachlich zur Schadenbewertung gehören.

Weiterführend

Verwandte Ansätze

  • Separation of Concerns: der Grundsatz, jede Aufgabe in genau einem Programmteil zu erledigen.
  • Bounded Context: ein fachlicher Bereich mit eigenen Begriffen. Er eignet sich gut als Grenze mit hohem Zusammenhalt innen.
  • Modular Monolith: setzt den Grundsatz innerhalb eines Programms um. Automatisch geprüfte Regeln sorgen dafür, dass die Programmteile die Grenzen zwischen Fachgebieten einhalten, ohne Aufrufe über das Netz.
  • Microservices: machen dieselben Grenzen zu Grenzen zwischen eigenständig ausgelieferten Diensten.
  • Event Driven Architecture: Bauweise, in der Programme sich über Ereignisse informieren, sodass der Absender die Empfänger nicht kennen muss.
  • Conway's Law: beschreibt, dass die Abhängigkeiten im Code oft den Abhängigkeiten zwischen Teams folgen.

Werkzeuge und Umsetzungen

Diese Projekte machen Abhängigkeiten zwischen Programmteilen sichtbar und prüfbar:

  • ArchUnit: prüft in automatischen Tests Abhängigkeiten zwischen Programmteilen in Java-Code.
  • jQAssistant: speichert den Aufbau des Codes als Netz aus Knoten und Verbindungen, einen sogenannten Graphen, und erlaubt Abfragen nach unerwünschten Abhängigkeiten.
  • Spring Modulith: prüft in Spring-Anwendungen die Grenzen fachlicher Programmteile und erzeugt Übersichten ihrer Abhängigkeiten.
  • dependency cruiser: zeigt und prüft Abhängigkeiten in JavaScript- und TypeScript-Projekten.

Quellen

  • Martin Fowler: Reducing Coupling: Beitrag in IEEE Software von 2001, der Kopplung als Änderungsabhängigkeit zwischen Modulen beschreibt, doppelten Code als Ursache nennt und zeigt, wie Schnittstellen unerwünschte Abhängigkeiten auflösen.
  • Microsoft: Identify microservice boundaries: nennt lose Kopplung und hohen fachlichen Zusammenhalt als Prüfkriterien für den Zuschnitt von Diensten.