Read in English · Leer en español · Lire en français
Abschnitt 3.1 von 3.8 · Mittelstufe · 22 Min. Lesezeit
Modularität: ein System in Teile zerlegen
Videolektion
5 Min. · auf Englisch gesprochen, mit UntertitelnNur anhören (Audio)
Transkript lesen
Jedes große System ist in Teile gegliedert. In dieser Lektion lernen Sie, was ein Modul ist, warum wir Systeme in Module gliedern und welche Regel am wichtigsten ist, um zu entscheiden, wo die Grenzen verlaufen.
Denken Sie an ein großes Krankenhaus. Es ist kein riesiger Raum, in dem alle alles tun. Es ist in Abteilungen gegliedert, etwa Radiologie und Apotheke, jede mit eigenem Personal. Andere haben mit jeder Abteilung über einen Empfang zu tun, mit klaren Anforderungsformularen. Wenn also die Apotheke ihr Lagersystem ändert, merken die Stationen nichts, weil sie weiterhin dasselbe Formular ausfüllen. Software wird auf die gleiche Weise in Module gegliedert. Gut gemacht, kann man ein Modul verstehen, ohne den Rest zu lesen, Teams können parallel arbeiten, und eine Änderung bleibt an einer Stelle. Aber nur, wenn die Wände an den richtigen Stellen stehen.
Ein Modul ist ein Teil eines Systems mit einer klaren Verantwortlichkeit und einer festgelegten Art, wie andere Teile es nutzen. Über die Größe sagt das nichts: Ein Modul kann eine Funktion, eine Klasse, ein Paket, eine Komponente oder ein ganzer Service sein. Seine Schnittstelle ist das, was andere wissen müssen, um es zu nutzen. Seine Implementierung ist alles andere. Gute Modularität hält die Schnittstelle klein und stabil, damit sich die Implementierung frei ändern kann.
Im Jahr neunzehnhundertzweiundsiebzig zeigte David Parnas an einem kleinen Programm, das einen Key-Word-in-Context-Index erstellt, mit dem man einen Titel unter jedem seiner Wörter findet, wie sehr die Wahl der Grenzen zählt. Das erste Design folgt dem Flussdiagramm, mit einem Modul pro Verarbeitungsschritt: Eingabe, zyklische Verschiebung, Alphabetisieren und Ausgabe. Das ist ein völlig vernünftiges Design. Aber jedes Modul kennt die gemeinsamen Datenformate im Speicher. Eine Änderung an der Speicherung der Zeilen berührt also alle vier. Das zweite Design wirkt seltsamer. Seine Module entsprechen nicht den Schritten. Stattdessen verbirgt jedes eine einzige Designentscheidung, etwa das Eingabeformat oder wann sortiert wird, hinter Zugriffsfunktionen. Jetzt bleibt eine Änderung an der Speicherung der Zeilen in einem Modul, dem Zeilenspeicher. Die anderen rufen nur seine Funktionen auf.
Parnas kam zu dem Schluss, dass es fast immer falsch ist, von einem Flussdiagramm auszugehen. Listen Sie stattdessen die Designentscheidungen auf, die schwierig sind oder sich wahrscheinlich ändern, und geben Sie jedem Modul eine davon zum Verbergen. Er nannte das Information Hiding. Ein Flussdiagramm beschreibt Zeit: erst dies, dann das. Aber Designentscheidungen liegen quer zur Zeit. Wie eine Bestellung dargestellt wird, spielt eine Rolle, wenn sie eingeht, bepreist, bezahlt und gespeichert wird. Besitzt ein Modul diese Entscheidung, lässt sie sich an einer Stelle ändern.
Als Nächstes eine wichtige Unterscheidung. Ein logisches Modul ist eine Verantwortungseinheit im Design, etwa Katalog oder Zahlungen. Ein physisches Modul ist eine Einheit, die gebaut und deployt wird, etwa ein Paket oder ein Container-Image. Dieselben logischen Module lassen sich als eine Einheit deployen, die sich prozessintern aufrufen: schnell, mit einem Release. Das ist ein modularer Monolith. Oder jedes kann ein eigener Service sein, der über das Netzwerk aufgerufen wird. Jeder lässt sich für sich releasen, aber Sie zahlen mit Latenz, Teilausfällen und Versionierung. Wer ein schlecht gegliedertes System in Services aufteilt, verbirgt damit nichts. Man bekommt nur verflochtene Services, manchmal verteilter Monolith genannt. Ziehen Sie zuerst die logischen Grenzen richtig, und entscheiden Sie erst dann, welche es verdienen, physisch zu werden.
Prüfen wir das am Checkout eines Onlineshops. Ein Design hat sechs Flussdiagrammschritte, die sich einen Bestelldatensatz teilen. Das andere hat sechs Module, die jeweils eine Entscheidung verbergen. Wir zählen die Module, die fünf erwartete Änderungen berühren würden. Ein zweiter Zahlungsanbieter berührt zwei Flussdiagrammschritte, aber nur das Modul Zahlungsgateway. Eine Änderung der Mehrwertsteuerregeln berührt drei Schritte, aber nur das Modul Steuer. Die Lieferadresse in strukturierte Felder aufzuteilen berührt alle sechs Schritte, weil sie alle den gemeinsamen Datensatz lesen. Im anderen Design ändert sich nur Bestellung. Manche Änderungen kosten in beiden Fällen gleich viel. SMS-Bestätigungen berühren in beiden ein Modul, und ein wirklich neues Feature, eine Grußbotschaft, berührt in beiden drei. Insgesamt fünfzehn gegen sieben. Wahrscheinliche Änderungen aufzulisten und die berührten Module zu zählen, ist eine schnelle Methode, um Designs in einem Review zu vergleichen.
Wie groß sollte ein Modul sein? Zu große Module vermischen voneinander unabhängige Änderungsgründe, und viele Personen bearbeiten dieselben Dateien. Zu kleine Module vervielfachen die Schnittstellen, sodass ein kleines Feature Änderungen an vielen Stellen braucht. Über ein Netzwerk kostet jede Schnittstelle zudem Latenz, Versionierung und eine neue Ausfallart, daher sollten Services viel gröber sein als Klassen. Und eine Grenze, die nur in einem Diagramm existiert, übersteht keinen Termindruck. Fachleute setzen sie mit Sichtbarkeitsregeln, Modulsystemen und automatisierten Abhängigkeitsprüfungen durch, die den Build scheitern lassen.
Der vollständige Abschnitt behandelt Parnas’ Beispiel im Detail und wie jede Sprache Grenzen durchsetzt. Als Nächstes betrachten wir in Abschnitt drei Punkt zwei Kopplung und Kohäsion. Bis gleich.
Was Sie wissen müssen
- Ein Modul ist eine Zerlegungseinheit mit einer Verantwortlichkeit und einer Schnittstelle: eine Funktion, Klasse, ein Paket, eine Komponente oder ein Service, je nachdem, auf welcher Ebene Sie arbeiten.
- Modularität zahlt sich aus durch Verständlichkeit, parallele Arbeit von Teams, Isolierung von Änderungen, Wiederverwendung und Tests, aber nur, wenn die Grenzen an den richtigen Stellen gezogen werden.
- David Parnas (1972) zeigte am KWIC-Beispiel, dass eine Zerlegung nach Verarbeitungsschritten jede Designentscheidung über viele Module verteilt, und schlug stattdessen vor, dass jedes Modul eine Entscheidung verbirgt, die schwierig ist oder sich wahrscheinlich ändert.
- Logische Module (Verantwortungseinheiten im Design) unterscheiden sich von physischen Modulen (Einheiten für Build und Deployment); dasselbe logische Design kann als eine Einheit oder als viele Services deployt werden.
- Granularität ist eine Abwägung: Zu große Module vermischen voneinander unabhängige Änderungen, zu kleine Module vervielfachen Schnittstellen, Koordination und über ein Netzwerk hinweg auch Latenz und Ausfallarten.
- Grenzen, die nicht durchgesetzt werden, erodieren; deshalb nutzen Fachleute die Sichtbarkeitsregeln der Sprache, Modulsysteme wie das Java Platform Module System, Module im Build-Werkzeug und automatisierte Abhängigkeitsprüfungen.
Was ein Modul ist
Ein Modul ist ein Teil eines Systems mit einer klaren Verantwortlichkeit und einer festgelegten Art, wie andere Teile es nutzen. Diese Definition ist bewusst unabhängig von der Größe. In Abschnitt 0.3 war ein Modul eine Python-Datei; in diesem Kapitel umfasst das Wort jede Einheit der Zerlegung, also des Aufteilens eines Systems in Teile: eine Funktion, eine Klasse, ein Paket, eine Bibliothek, eine Komponente innerhalb einer Anwendung oder einen separat deployten Service. Auf jeder Ebene stellen sich dieselben Fragen: Wofür ist dieser Teil verantwortlich, was bietet er anderen an, was behält er für sich, und wovon hängt er ab?
Jedes Modul hat zwei Gesichter. Seine Schnittstelle ist das, was andere Module wissen müssen, um es zu nutzen: Funktionsnamen und Parameter, eine HTTP-API, die Ereignisse, die es veröffentlicht, und auch die informellen Zusagen, etwa wie schnell es antwortet und welche Fehler es zurückgeben kann. Seine Implementierung ist alles andere: der Code, die Datenstrukturen, die Datenbanktabellen, die Drittanbieter-Bibliotheken, die es zufällig verwendet. Gute Modularität hält die Schnittstelle klein und stabil und lässt die Implementierung sich frei ändern.
David Parnas, dessen Artikel von 1972 die Grundlage dieses Abschnitts ist, definierte ein Modul als Zuweisung einer Verantwortlichkeit statt als Stück Programmtext. Für Architekturfachleute ist das die nützlichste Sichtweise. Ein Modul ist ein Versprechen, dass ein Teil des Systems, und meist ein Team, für eine bestimmte Menge von Entscheidungen verantwortlich ist. Ob der Code am Ende in einer Datei oder in vierzig liegt, in einem Prozess oder in zehn, ist eine separate Frage, die weiter unten unter logischen und physischen Modulen behandelt wird.
Warum überhaupt modularisieren
Ein System aus ein paar hundert Zeilen lässt sich als Ganzes verstehen. Ein System aus ein paar Millionen Zeilen nicht, und auch die Organisation, die es baut, kann nicht als eine ungeteilte Gruppe arbeiten. Modularität ist die Eigenschaft, in Module gegliedert zu sein, die sich weitgehend für sich allein verstehen, ändern und durchdenken lassen. Sie ist eines der fünf Teilmerkmale der Wartbarkeit in ISO/IEC 25010 (Abschnitt 2.6) und unterstützt mindestens fünf verschiedene Vorteile.
Verständlichkeit. Eine Entwicklerin oder ein Entwickler kann ein Modul und seine Schnittstellen studieren, ohne den Rest des Systems zu lesen. Parallele Arbeit. Teams können gleichzeitig an verschiedenen Modulen arbeiten und müssen sich kaum abstimmen, weil sie die Schnittstellen vorab vereinbart haben. Isolierung von Änderungen. Eine Änderung an einer Designentscheidung, etwa welche Steuerregeln gelten, erfolgt in einem Modul und breitet sich nicht nach außen aus. Wiederverwendung. Ein gut abgegrenztes Modul, etwa eine Komponente zur Adressvalidierung, kann von mehreren Produkten genutzt werden. Tests. Ein Modul mit einer klaren Schnittstelle lässt sich für sich allein testen, wobei seine Abhängigkeiten durch Test Doubles ersetzt werden.
Parnas nannte 1972 drei davon: den organisatorischen Vorteil (kürzere Entwicklungszeit, weil getrennte Gruppen mit wenig Kommunikation an getrennten Modulen arbeiten), Flexibilität (drastische Änderungen an einem Modul, ohne andere zu ändern) und Verständlichkeit (das System Modul für Modul studieren). Ein halbes Jahrhundert später sind das immer noch die Gründe. Beachten Sie aber, dass keiner davon automatisch daraus folgt, dass man ein System in Stücke schneidet. Ein System kann in fünfzig Module gegliedert sein und trotzdem keinen dieser Vorteile liefern, wenn jede Änderung zehn davon berührt. Die Vorteile hängen ganz davon ab, wo die Grenzen gezogen werden.
Parnas und das KWIC-Beispiel
Parnas veranschaulichte das an einem kleinen Programm namens KWIC (Key Word in Context), das einen Index von Titeln erzeugt. Es liest Textzeilen ein, erzeugt jede zyklische Verschiebung jeder Zeile (indem es wiederholt das erste Wort ans Ende stellt), sortiert alle Verschiebungen alphabetisch und gibt sie aus. Die Zeile Design of Software ergibt Design of Software, of Software Design und Software Design of, sodass eine Leserin oder ein Leser den Titel unter jedem seiner Wörter finden kann.
Die erste Zerlegung, die er beschreibt, ist die, die die meisten Programmierenden beim Zeichnen eines Flussdiagramms erzeugen würden: ein Eingabemodul, das die Zeilen liest und in einem gepackten Format im Speicher ablegt, ein Modul für die zyklische Verschiebung, das einen Index der Startpositionen jeder Verschiebung aufbaut, ein Alphabetisierungsmodul, das diesen Index sortiert, ein Ausgabemodul, das das Ergebnis ausgibt, und ein Hauptsteuermodul, das sie der Reihe nach aufruft. Das ist ein völlig vernünftiges Design. Jedes Modul ist klein, und die Schnittstellen zwischen ihnen sind die Datenformate im Speicher.
Die zweite Zerlegung wirkt seltsamer. Sie hat ein Zeilenspeichermodul, das Funktionen wie liefere das Zeichen in Zeile r, Wort w, Position c anbietet und verbirgt, wie die Zeilen tatsächlich gespeichert sind; ein Eingabemodul, das Zeilen liest und an den Zeilenspeicher übergibt; einen zyklischen Verschieber, der die Verschiebungen über ähnliche Zugriffsfunktionen so präsentiert, als ob sie gespeichert wären; einen Alphabetisierer, der gib mir die i-te Verschiebung in der Reihenfolge anbietet, ohne zu sagen, wann oder wie er sortiert; ein Ausgabemodul und eine Hauptsteuerung. Die Module entsprechen nicht mehr den Schritten der Verarbeitung.
Parnas listete dann Entscheidungen auf, die fragwürdig waren und sich wahrscheinlich ändern würden: das Eingabeformat; alle Zeilen im Speicher zu halten, was bei sehr großen Eingaben unpraktikabel ist; mehrere Zeichen in jedes Maschinenwort zu packen; einen Index der Verschiebungen statt der Verschiebungen selbst zu speichern; und alles einmal zu sortieren, statt bei Bedarf zu suchen. In der ersten Zerlegung betreffen die meisten dieser Änderungen mehrere Module, weil jedes Modul die gemeinsamen Datenformate kennt. In der zweiten bleibt jede Änderung auf das eine Modul beschränkt, das diese Entscheidung verbirgt. Das zweite Design ließ sich auch leichter parallel entwickeln, weil sich die Teams nur auf Funktionsnamen und Parameter einigen mussten, nicht auf detaillierte Tabellenlayouts, und es ließ sich leichter Modul für Modul verstehen.
Parnas’ zwei Zerlegungen von KWIC
Das Kriterium: nach Entscheidungen zerlegen, die sich wahrscheinlich ändern
Parnas kam zu dem Schluss, dass es fast immer falsch
ist, die Zerlegung eines Systems auf der Grundlage eines Flussdiagramms zu beginnen. Stattdessen schlug er vor, mit einer Liste schwieriger Designentscheidungen oder solcher, die sich wahrscheinlich ändern, zu beginnen und jedes Modul so zu entwerfen, dass es eine solche Entscheidung vor den anderen verbirgt. Er nannte das Prinzip Information Hiding; Abschnitt 3.3 behandelt es ausführlich.
Die Idee klingt einfach, kehrt aber den Instinkt der meisten von uns um. Ein Flussdiagramm beschreibt Zeit: Erst passiert dies, dann das. Designentscheidungen liegen quer zur Zeit. Die Entscheidung, wie eine Bestellung dargestellt wird, spielt eine Rolle, wenn die Bestellung eingeht, bepreist, bezahlt, gespeichert und bestätigt wird. Folgen die Module den Schritten, ist diese eine Entscheidung über alle verteilt, und wer sie ändert, muss alle ändern. Besitzt ein Modul die Entscheidung, fragen alle Schritte dieses Modul, und die Entscheidung lässt sich an einer Stelle ändern.
In der Praxis kommt es auf zwei Präzisierungen an. Erstens geht es beim Kriterium um wahrscheinliche Änderungen; Architekturfachleute müssen also eine fundierte Vermutung über die Zukunft anstellen: Was hat sich in ähnlichen Systemen geändert, was plant das Unternehmen, welche Lieferanten und Vorschriften sind instabil? Die vier Fragen zur Änderbarkeit aus Abschnitt 2.6 (was ändert sich, wie wahrscheinlich, wer ändert es, zu welchen Kosten) liefern genau die Eingaben, die dafür nötig sind. Zweitens unterschied Parnas eine gute Zerlegung von einer Hierarchie. Ein System kann sauber geschichtet und trotzdem schlecht zerlegt sein, und umgekehrt; Abschnitt 3.4 behandelt die Richtung von Abhängigkeiten gesondert.
Logische und physische Module
Ein logisches Modul ist eine Verantwortungseinheit im Design: Katalog, Bestellungen, Zahlungen. Ein physisches Modul ist eine Einheit, die gebaut, versioniert oder deployt wird: eine Java-JAR-Datei, ein Python-Wheel, ein npm-Paket, ein Container-Image, eine Serverless-Funktion. Abschnitt 1.1 nannte diese Modulstrukturen und Zuordnungsstrukturen. Beide hängen zusammen, sind aber nicht dasselbe, und wer sie verwechselt, begeht einige der teuersten Fehler moderner Architektur.
Dasselbe logische Design lässt sich auf sehr unterschiedliche Weise auf physische Module abbilden. Alle logischen Module können in einer deploybaren Einheit leben und sich prozessintern gegenseitig aufrufen: ein modularer Monolith. Oder jedes kann als eigener Service deployt werden und die anderen über das Netzwerk aufrufen: Microservices. Die logischen Grenzen und die Entscheidungen, die jedes Modul verbirgt, können in beiden Fällen identisch sein. Was sich ändert, sind die Kosten, eine Grenze zu überqueren (ein Funktionsaufruf dauert Nanosekunden, ein Netzwerkaufruf Millisekunden und kann fehlschlagen), die Möglichkeit, Teile unabhängig zu releasen und zu skalieren, und der Betriebsaufwand.
Ein logisches Design, zwei physische Designs
Ein häufiger und kostspieliger Fehler ist es, physische Trennung so zu behandeln, als würde sie logische Modularität schaffen. Wer ein schlecht zerlegtes System in Services aufteilt, verbirgt damit keine einzige Entscheidung; er macht aus prozessinternen Aufrufen zwischen verflochtenen Modulen Netzwerkaufrufe zwischen verflochtenen Services, was manchmal als verteilter Monolith bezeichnet wird. Auch der umgekehrte Fehler kommt vor: Ein Team behält eine deploybare Einheit, lässt aber ihre internen Module ineinanderwachsen, sodass es sie später nie mehr trennen kann. Der praktische Rat vieler Fachleute lautet, zuerst die logischen Grenzen richtig zu ziehen, sie durchzusetzen und erst dann zu entscheiden, welche davon es verdienen, zu physischen Grenzen zu werden.
Granularität und ihre Kosten
Wie groß sollte ein Modul sein? Granularität ist die Größe von Modulen im Verhältnis zum System, und es gibt keine allgemeingültige richtige Antwort, weil die Kosten in entgegengesetzte Richtungen ziehen. Größere Module haben weniger Schnittstellen, die man entwerfen und pflegen muss, aber jedes enthält mehr voneinander unabhängige Änderungsgründe, mehr Personen, die gleichzeitig daran arbeiten, und mehr, was man verstehen muss, bevor man etwas ändert. Kleinere Module sind jeweils leichter zu verstehen, aber das System hat mehr Schnittstellen, mehr Abhängigkeiten, die verwaltet werden müssen, und mehr Stellen, an denen ein einzelnes Feature zusammengeflickt werden muss.
Die Kosten einer Schnittstelle hängen auch von der Art der Grenze ab. Zwischen zwei Klassen in einem Programm kostet eine zusätzliche Schnittstelle ein wenig Lesezeit. Zwischen zwei Services kostet sie einen Netzwerksprung, Serialisierung, eine Versionierungsrichtlinie, Monitoring, eine Ausfallart und möglicherweise die Zustimmung eines zweiten Teams zu jeder Änderung. Deshalb ist die richtige Granularität für Services meist viel gröber als die richtige Granularität für Klassen.
Symptome falscher Granularität
| Zu grob | Zu fein |
|---|---|
| Voneinander unabhängige Features ändern dasselbe Modul; viele Personen bearbeiten dieselben Dateien und geraten in der Versionskontrolle in Konflikt. | Ein einzelnes kleines Feature erfordert abgestimmte Änderungen an vielen Modulen oder Services. |
| Lange Build- und Testzeiten bei jeder Änderung; niemand versteht das ganze Modul. | Viele triviale durchreichende Schnittstellen, die keine Abstraktion hinzufügen; Aufrufketten über mehrere Module hinweg. |
| Teams stehen für ein Release Schlange, weil alles zusammen ausgeliefert wird. | Über ein Netzwerk: hohe Latenz durch gesprächige Aufrufe, Teilausfälle und verteilte Transaktionen. |
Der Name des Moduls ist vage, etwa core, common oder manager. | Jedes Modul ist flach: Seine Schnittstelle ist fast so komplex wie seine Implementierung (siehe Abschnitt 3.3). |
Modulgrenzen im Code durchsetzen
Eine Grenze, die nur in einem Diagramm existiert, übersteht keinen Termindruck. Früher oder später importiert jemand eine interne Klasse, weil es schneller geht, und sobald es eine Abkürzung gibt, folgen weitere; Abschnitt 1.7 nannte das Erosion. Sprachen und Build-Werkzeuge bieten mehrere Möglichkeiten, die beabsichtigte Grenze zum Weg des geringsten Widerstands zu machen oder dafür zu sorgen, dass ihr Überschreiten den Build scheitern lässt.
Python setzt auf Konventionen: Namen mit führendem Unterstrich gelten als intern (PEP 8), und die __init__.py eines Pakets kann nur seine öffentlichen Funktionen re-exportieren. Nichts verhindert einen Import von shop.payments._stripe_client, daher setzen Teams, denen das wichtig ist, ein Prüfwerkzeug wie import-linter ein, das den Build scheitern lässt, sobald ein verbotener Import auftaucht. Java bietet Package-private-Sichtbarkeit (der Standard, wenn kein Modifizierer angegeben ist) und seit Java 9 im Jahr 2017 das Java Platform Module System: Eine Datei module-info.java deklariert, welche Module dieses Modul benötigt (requires) und welche seiner Pakete es exportiert (exports); nicht exportierte Pakete lassen sich von außen nicht nutzen, weder zur Kompilierzeit noch zur Laufzeit. Go behandelt jedes Paket unterhalb eines Verzeichnisses namens internal so, dass es nur von Code importiert werden kann, der im übergeordneten Verzeichnis dieses Verzeichnisses wurzelt. C# hat internal, sichtbar nur innerhalb einer Assembly; Rust hat pub(crate).
# shop/payments/__init__.py (the public face of the package)
from shop.payments.api import charge, refund, PaymentResult
__all__ = ["charge", "refund", "PaymentResult"]
# shop/payments/_provider_client.py internal: leading underscore
# shop/payments/_retry.py internal
# shop/payments/api.py the only module callers should use
# Elsewhere in the shop:
from shop.payments import charge # fine
from shop.payments._retry import backoff # works, but breaks the boundary// module-info.java for the payments module
module shop.payments {
requires shop.orders.api; // what this module depends on
exports shop.payments.api; // the only package others can use
// shop.payments.provider and shop.payments.retry are not exported,
// so code in other modules cannot compile against them
}Möglichkeiten, eine Grenze durchzusetzen
| Mechanismus | Beispiele | Stärke |
|---|---|---|
| Namenskonvention | Führender Unterstrich in Python; internal im Namen eines Namensraums | Schwach: setzt auf Disziplin und Code-Review |
| Sichtbarkeit in der Sprache | Package-private in Java, internal in C#, pub(crate) in Rust, internal-Verzeichnisse in Go | Stark innerhalb eines Builds; vom Compiler geprüft |
| Modulsystem der Sprache | Java Platform Module System (requires, exports); das Feld exports in einer Node.js-package.json | Stark: nicht deklarierter Zugriff scheitert beim Kompilieren oder Laden |
| Module im Build-Werkzeug | Multi-Modul-Builds mit Maven oder Gradle; Sichtbarkeitsregeln in Bazel; separate Pakete in einem Monorepo | Stark: Ein Modul kann nur nutzen, was es als Abhängigkeit deklariert |
| Automatisierte Architekturprüfungen | import-linter, ArchUnit, dependency-cruiser (Abschnitt 3.4) | Stark, wenn sie in der Build-Pipeline als Fitness Function laufen |
| Prozess- und Netzwerkgrenze | Separate Services mit eigenen Datenspeichern, nur über eine API erreichbar | Am stärksten, aber am teuersten zu überqueren und zu ändern |
Ausgearbeitetes Beispiel
Ausgearbeitetes Beispiel: einen Onlineshop nach Flussdiagramm und nach wahrscheinlichen Änderungen zerlegen
Der Checkout eines Onlineshops wird zuerst als Flussdiagramm entworfen: Bestellung entgegennehmen, Warenkorb validieren, Preis berechnen (Aktionen und Mehrwertsteuer), Zahlung entgegennehmen, Bestellung speichern, Bestätigung senden. Alle sechs Schritte lesen und schreiben einen gemeinsamen Bestelldatensatz, ein Dictionary, dessen Schlüssel jeder Schritt kennt. Das alternative Design hat sechs Module, die jeweils eine Entscheidung verbergen: Bestellung (wie eine Bestellung dargestellt und gespeichert wird), Preisgestaltung (Aktionsregeln), Steuer (Mehrwertsteuerregeln), Zahlungsgateway (welcher Anbieter und wie er aufgerufen wird), Benachrichtigungen (Kanäle und Vorlagen) und ein Checkout-Workflow, der nur die Reihenfolge der Aufrufe festlegt, wie Parnas’ Hauptsteuerung. Fünf Änderungen, die das Unternehmen im nächsten Jahr erwartet, werden für beide Designs durchgezählt.
| Änderung 1: einen zweiten Zahlungsanbieter hinzufügen | Flussdiagramm: Zahlung entgegennehmen sowie Bestellung speichern, das das Format der Transaktionsreferenz des Anbieters speichert = 2 Module. Nach Änderungen: Zahlungsgateway = 1. |
|---|---|
| Änderung 2: Die Mehrwertsteuerregeln für eine Produktkategorie ändern sich | Flussdiagramm: Preis berechnen, Bestellung speichern (Mehrwertsteuerspalten) und Bestätigung senden (Aufschlüsselung der Mehrwertsteuer) = 3. Nach Änderungen: Steuer = 1. |
| Änderung 3: die Lieferadresse in strukturierte Felder aufteilen | Flussdiagramm: Jeder Schritt liest den gemeinsamen Bestelldatensatz = 6. Nach Änderungen: Bestellung = 1 (die anderen rufen ihre Methoden auf). |
| Änderung 4: SMS-Bestätigungen hinzufügen | Flussdiagramm: Bestätigung senden = 1. Nach Änderungen: Benachrichtigungen = 1. |
| Änderung 5: neues Feature, eine Grußbotschaft zu Bestellungen | Flussdiagramm: Bestellung entgegennehmen, Bestellung speichern, Bestätigung senden = 3. Nach Änderungen: Checkout-Workflow, Bestellung, Benachrichtigungen = 3. |
| Insgesamt berührte Module | Flussdiagramm: 2 + 3 + 6 + 1 + 3 = 15. Nach Änderungen: 1 + 1 + 1 + 1 + 3 = 7. |
Änderungen an bestehenden Entscheidungen (Anbieter, Steuerregeln, Darstellung) bleiben im änderungsbasierten Design innerhalb eines Moduls; genau das hat Parnas vorhergesagt.
Manche Änderungen kosten in beiden Designs gleich viel (SMS), und ein wirklich neues Feature (Grußbotschaft) überquert in beiden Modulgrenzen. Information Hiding schützt Entscheidungen, die Sie vorhergesehen haben, nicht jedes mögliche Feature.
Das Zählen ist eine schnelle, wiederholbare Methode, um Zerlegungskandidaten in einem Design-Review zu vergleichen: Listen Sie zuerst die wahrscheinlichen Änderungen auf und zählen Sie dann die Module, die jede davon berührt.
Häufige Fehler
Nur nach technischen Schichten zerlegen. Ordner namens controllers, services und repositories sind gewissermaßen Module, aber eine Änderung an der Mehrwertsteuerberechnung berührt alle drei. Schichten sind nützlich (Abschnitt 3.4), doch eine Zerlegung, die nur aus Schichten besteht, ist ein verkapptes Flussdiagramm. Die meisten ausgereiften Systeme kombinieren eine Aufteilung nach Geschäftsfähigkeiten mit Schichtenbildung innerhalb jedes Teils.
Alles gemeinsam nutzen. Ein Modul namens common oder utils, das jedes andere Modul importiert, wird zu dem Ort, an dem sich voneinander unabhängige Entscheidungen ansammeln, und jede Änderung daran breitet sich überallhin aus. Grenzen ohne Durchsetzung. Diagramme zeigen saubere Module, während der Code Hunderte von Querimporten enthält. Module, die durch das Organigramm einer früheren Umstrukturierung definiert sind statt durch die Entscheidungen, die das System verbergen muss; Abschnitt 3.7 erklärt, warum Organisationen und Modulgrenzen aneinander ziehen. Verfrühte Verteilung. Wer jedes logische Modul zu einem Service macht, bevor sich die Grenzen als stabil erwiesen haben, macht spätere Grenzkorrekturen teuer.
Checkliste: eine vorgeschlagene Zerlegung prüfen
- Benennen Sie die Entscheidung, die jedes Modul verbirgtWenn Sie nicht in einem Satz sagen können, was ein Modul geheim hält, ist es wahrscheinlich ein Verarbeitungsschritt oder ein Sammelsurium.
- Listen Sie die wahrscheinlichen Änderungen aufSchreiben Sie fünf bis zehn Änderungen auf, die das Geschäft oder die Technologie wahrscheinlich mit sich bringen, und zählen Sie die Module, die jede davon berühren würde.
- Prüfen Sie die Größe der SchnittstelleDie Schnittstelle sollte viel kleiner sein als die Implementierung. Große Schnittstellen mit wenig Verhalten dahinter deuten auf den falschen Schnitt hin.
- Trennen Sie Logisches von PhysischemEinigen Sie sich zuerst auf die logischen Module; entscheiden Sie separat, welche davon eine eigene deploybare Einheit verdienen und warum.
- Legen Sie fest, wie die Grenze durchgesetzt wirdWählen Sie einen Sichtbarkeitsmechanismus oder eine automatisierte Prüfung und fügen Sie sie dem Build hinzu, bevor die erste Abkürzung auftaucht.
- Richten Sie Grenzen an Zuständigkeiten ausJedes Modul sollte ein verantwortliches Team haben; Module, die mehrere Teams für den Großteil ihrer Arbeit gemeinsam nutzen, sind ein Warnsignal.
Wie Fachleute Modularität anwenden
Erfahrene Architekturfachleute behandeln die Modulstruktur als das wichtigste Designartefakt für die Wartbarkeit, und sie überprüfen sie immer wieder. Früh im Lebenszyklus eines Produkts, wenn die Domäne noch wenig verstanden ist, bevorzugen sie weniger, größere Module mit starken internen Grenzen, weil das Zusammenführen und Aufteilen von Modulen innerhalb einer Codebasis billig ist. Wenn sich die Domäne stabilisiert und wahrscheinliche Änderungen klar werden, verfeinern sie die Grenzen und überlegen erst dann, welche davon separat deployt werden sollten. Techniken aus dem Domain-Driven Design, etwa Bounded Contexts, helfen, Grenzen zu finden, die der Sprache des Fachbereichs folgen.
Sie messen auch. Wer die Module zählt, die von jüngsten Änderungen berührt wurden, was mit der Historie der Versionskontrolle leicht geht, sieht, ob die Grenzen funktionieren; Abschnitt 3.8 führt Change Coupling und Hotspots ein. Der nächste Abschnitt, Abschnitt 3.2, liefert das Vokabular, um zu beschreiben, wie gute und schlechte Grenzen aussehen: Kopplung zwischen Modulen und Kohäsion innerhalb von Modulen.
Prüfen Sie Ihr Verständnis
Warum nannte Parnas ein Modul eine „Zuweisung einer Verantwortlichkeit“ und nicht ein Unterprogramm?
Weil es darauf ankommt, für welche Designentscheidungen ein Modul verantwortlich ist und welche es verbirgt, nicht darauf, wie sein Code verpackt ist; eine Verantwortlichkeit kann mehrere Routinen umfassen, und die Routinen müssen nicht den Verarbeitungsschritten entsprechen.
Warum betraf im KWIC-Beispiel eine Änderung an der Speicherung der Zeilen in der ersten Zerlegung mehrere Module?
Alle Module nutzten die Datenformate im Speicher direkt gemeinsam, daher war die Speicherentscheidung jedem Modul bekannt, statt in einem verborgen zu sein.
Was ist der Unterschied zwischen einem logischen und einem physischen Modul?
Ein logisches Modul ist eine Verantwortungseinheit im Design; ein physisches Modul ist eine Einheit, die gebaut oder deployt wird, etwa eine JAR-Datei, ein Paket, ein Container oder ein Service. Ein logisches Design lässt sich physisch auf viele Arten verpacken.
Nennen Sie zwei Anzeichen dafür, dass Services zu feingranular sind.
Kleine Features erfordern abgestimmte Änderungen über mehrere Services hinweg, und Anfragen durchlaufen gesprächige Ketten von Netzwerkaufrufen mit hoher Latenz und Teilausfällen.
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 entscheide ich, wo ich in einem bestehenden Monolithen Modulgrenzen ziehe, bevor wir überhaupt daran denken, ihn in Services aufzuteilen?“