Architektur-Management für Java-Projekte: Prinzipien & Grundlagen

Messung des Kopplungsgrades

Eine weitere Größe, die man stets im Auge haben sollte, ist der Kopplungsgrad des Systems. Als Faustregel gilt, dass ein System umso schwerer zu ändern ist, je höher der Kopplungsgrad ist. Denn mit wachsendem Kopplungsgrad wächst auch die Anzahl der Codestellen, die von einer durchzuführenden Codeänderung direkt oder indirekt betroffen wären. Daher ist es stets vorteilhaft, den Kopplungsgrad so gering wie möglich zu halten.

Messen kann man den Kopplungsgrad mit der ACD-Metrik (Average Component Dependency) [John Lakos: Large-Scale C++ Software Design,
Addison-Wesley 1996]. Abbildung 3 zeigt ein Berechnungsbeispiel. Die kleinen Rechtecke (Knoten) stehen für Übersetzungseinheiten oder Typcontainer (Packages, Subsysteme etc.). Die Zahlen innerhalb der Knoten geben den sog. „depends upon“-Wert an. Diese Zahl besagt, von wie vielen anderen Knoten der gegebene Knoten direkt oder indirekt abhängt. Dabei wird der Knoten selbst mitgezählt, da per Definition jeder Knoten auch von sich selbst abhängt. Die ACD-Metrik wird nun berechnet, indem man die Summe der „depends-upon“-Werte aller Knoten durch die Anzahl der Knoten dividiert. Für das linke Beispiel ergibt sich ein Wert von 2,5. Dieser Wert besagt, dass im linken Graphen jeder Knoten im Mittel von 2,5 Knoten abhängt. Wenn man berücksichtigt, dass jeder Knoten immer von sich selbst abhängt, kann man auch sagen, dass in diesem Beispiel jeder Knoten im Mittel von sich selbst und 1,5 anderen Knoten abhängt.

Abb. 3: ACD-Berechnung

Das rechte Beispiel in Abbildung 3 zeigt, wie sich der Kopplungsgrad deutlich erhöht, wenn zyklische Abhängigkeiten mit ins Spiel kommen. Durch Hinzufügen einer einzigen Abhängigkeit entstehen zwei Abhängigkeitszyklen, einer von der Wurzel links herum und der andere rechts herum. Sobald Knoten zu einem Zyklus gehören, entspricht ihr „depends upon“-Wert der Anzahl der Zyklenteilnehmer (4) zuzüglich aller von den Zyklenteilnehmern aus direkt oder indirekt erreichbaren Knoten (2). Die vier Knoten im Zyklus sind nun untrennbar miteinander verbunden und können nur noch als Ganzes getestet, verstanden oder in Betrieb genommen werden. Der ACD-Wert steigt auf 4,33 – eine erheblich Verschlechterung gegenüber dem linken Graphen.

Der ACD selbst muss nun noch einmal durch die Anzahl der Knoten dividiert werden, um einen vergleichbaren Wert zu bekommen. Diesen Wert bezeichne ich als rACD (relative Average Component Dependency). Bei größeren Knotenzahlen (> 50) kann man theoretisch herleiten, dass der höchstmögliche Wert für die rACD in einem zyklenfreien Abhängigkeitsgraphen bei 0,5 (also 50 Prozent) liegt. Sobald die rACD in einem solchen Graphen also über 50 Prozent liegt, kann man mit Sicherheit davon ausgehen, dass Zyklen vorhanden sind.

Es bleibt die Frage, welche Werte die rACD auf der Ebene der Übersetzungseinheiten haben sollte, um den Kopplungsgrad eines Projekts als angemessen betrachten zu können. Hier kann man nur mit Erfahrungs- und Vergleichswerten arbeiten. Für Projekte ab 200 Übersetzungseinheiten sollte die rACD maximal bei 15 Prozent liegen, ab 1.000 Übersetzungseinheiten bei maximal 7,5 Prozent und ab 5.000 Übersetzungseinheiten bei höchstens 4 Prozent. Mit anderen Worten, der Kopplungsgrad sollte immer deutlich langsamer als die Anzahl der Übersetzungseinheiten wachsen.

Als mathematische Näherung kann man diese Heuristik wie in der folgenden Formel formulieren:

n ist dabei die Anzahl der Übersetzungseinheiten und sollte bei dieser Näherung mindestens bei 200 liegen. Bei kleineren Werten wächst rACD schneller als diese Näherung. Für Mathe-Muffel hier noch einmal die Bedeutung der Formel im Klartext: der Maximalwert von rACD halbiert sich, wenn die n um den Faktor 5 wächst.

Zyklen sind böse

Die Diskussion des zweiten Abhängigkeitsgraphen aus Abbildung 3 hat gezeigt, das Zyklen sich negativ auf den Kopplungsgrad auswirken. Andere negative Auswirkungen sind:

  • Zyklenteilnehmer können nicht mehr getrennt voneinander getestet werden. Damit wird das Erreichen einer guten Testabdeckung erheblich schwieriger, da man immer alle Zyklenteilnehmer berücksichtigen muss.
  • Das Codeverständnis wird erschwert, da man tendenziell alle Zyklenteilnehmer gemeinsam verstehen muss. Die zyklische Abhängigkeit verhindert, dass man sich von unten nach oben hocharbeiten kann.
  • Zyklen erhöhen die Komplexität.
  • Auch für den Betrieb sind Zyklen potenziell unangenehm, insbesondere wenn sie mehrere fachliche Komponenten umfassen. Denn in diesem Fall kann keine der Komponenten ohne die anderen in Betrieb genommen werden. Für eine gute logische Architektur empfiehlt sich daher folgende Designregel:

Zyklische Abhängigkeiten sind ab der Package-Ebene aufwärts verboten.

Diese Regel sollte sehr ernsthaft umgesetzt werden, da jedes zyklische Kon­strukt sich durch eine nicht zyklische Alternative ersetzen lässt. John Lakos hat diesem Thema in seinem Buch [John Lakos: Large-Scale C++ Software Design,
Addison-Wesley 1996] ein ganzes Kapitel gewidmet und man könnte allein darüber sicher einen eigenständigen Artikel schreiben. (Das Buch bezieht sich auf C++, die Lösungen lassen sich aber allesamt leicht auf Java übertragen.) Auch in der einschlägigen Literatur finden sich zahlreiche Argumente gegen zyklische Abhängigkeiten:

„Guideline: No Cycles between Packages. If a group of packages have cyclic dependency then they may need to be treated as one larger package in terms of a release unit. This is undesirable because releasing larger packages (or package aggregates) increases the likelihood of affecting something.“ [Craig Larman: Applying UML and Patterns,
Prentice-Hall 2002]
„The dependencies between packages must not form cycles.“ [Robert C. Martin: Agile Software Development, Prentice-Hall 2003.]

„Cyclic physical dependencies among components inhibit understanding, testing and reuse … Every directed a-cyclic graph can be assigned unique level numbers; a graph with cycles cannot … A physical dependency graph that can be assigned unique level numbers is said to be leveliz­able … In most real-world situations, large­ designs must be levelizable if they are to be tested effectively… Independent testing reduces part of the risk associated with software integration.“ [John Lakos: Large-Scale C++ Software Design,
Addison-Wesley 1996.]

Weitere Designregeln, die sich bewährt haben

Robert C. Martin hat ein viel zitiertes Papier geschrieben: „Design Principles and Design Patterns“, das ich jedem Softwarearchitekten nur wärmstens empfehlen kann. Eines der dort beschriebenen Prinzipien nennt sich „Dependency Inversion Principle“. Die Kernidee dieses Prinzips besagt, dass sich die Kopplung eines Systems verringert und die Flexibilität erhöht, wenn man die Richtung von Abhängigkeiten an geeigneter Stelle invertiert. Abbildung 4 zeigt dazu ein Beispiel.

Abb. 4: Dependency Inversion Principle

Das linke Diagramm zeigt eine klassische Dreischicht-Architektur. Das rechte Diagramm greift auf das „Dependency Inversion“-Prinzip zurück. In der Domain-Schicht werden dazu Interfaces definiert, die die Anforderungen der Domain-Schicht an die Datenzugriffsschicht definieren. Die Datenzugriffsschicht muss dann lediglich diese Interfaces implementieren. Anstelle einer klassischen „benutzt“-Beziehung haben wir nun eine invertierte „implements“-Beziehung. Die Flexibilität erhöht sich auch, da man verschiedene Implementierungen der Datenzugriffsschicht an dieselbe Domain Schicht anbinden kann. Für das Testen ist es z.B. oft sehr nützlich, mit Mock-Objekten zu arbeiten. Auch der Kopplungsgrad verringert sich, wie man durch Nachrechnen des ACD-Wertes leicht feststellen kann.

Ein weiteres wichtiges Designprinzip liegt darin, im Abhängigkeitsgraphen unten liegende Knoten möglichst abstrakt zu halten, also dort lieber mit Interfaces als mit konkreten Klassen zu arbeiten. Die Motivation dafür besteht einfach darin, dass Interfaces in aller Regel erheblich stabiler als Klassen sind und außerdem zu einer Verringerung des Kopplungsgrades beitragen. Die erfolgreiche Umsetzung dieses Prinzips lässt sich recht leicht mithilfe der definierten „Distance“-Metrik überprüfen.

Designregeln auf der Code-Ebene

Obwohl Architektur-Management sich schwerpunktmäßig mit der Gesamtstruktur einer Software befasst, ist es trotzdem sinnvoll, auch einige Regeln auf der Code-Ebene zu definieren. Diese Regeln dienen vor allem dazu, die Verständlichkeit und Testbarkeit des Codes zu gewährleisten und gehen nicht so sehr in die Details.
Die erste Regel limitiert die Größe von Übersetzungseinheiten. Bei hello2morrow arbeiten wir z.B. mit einem Limit von 700 Codezeilen („Lines of Code“-Metrik – alle Zeilen außer Leerzeilen und Kommentarzeilen), das sich bewährt hat. Die Motivation besteht vor allem darin zu verhindern, dass einzelne Klassen zu groß werden. Sobald das Limit überschritten wird, lohnt es sich, über eine Aufteilung der Logik in mehrere Klassen nachzudenken. Dies ist natürlich nicht immer möglich, aber es ist auf jeden Fall eine gute Idee, die Anzahl der Ausnahmen möglichst klein zu halten.

Eine weitere Regel limitiert die zyklomatische Komplexität von Java-Methoden. Diese Metrik berechnet die Anzahl möglicher Ablaufpfade in einer Methode. Diese Anzahl entspricht wiederum der Anzahl der Testfälle, die für eine hundertprozentige Testabdeckung benötigt werden. Wir benutzen 25 als Grenzwert und auch das hat sich bewährt.

In diesem Bereich kann man natürlich noch viele andere Regeln definieren, aber ich bin der Meinung, dass man versuchen sollte, die Anzahl der Regeln so klein wie möglich zu halten. Problematische Codestellen Komplexitätsbrennpunkte werden erfahrungsgemäß mit hoher Wahrscheinlichkeit durch die beiden oben definierten Regeln entdeckt.

Zusammenfassung der Regeln

Nun ist es an der Zeit, die bisher definierten Regeln zusammenzufassen, um dann im nächsten Schritt die Frage der effektiven Überprüfung im Projektverlauf zu klären. Bisher haben wir also folgende Prinzipien und Regeln aufgestellt:

  1. Jedes nicht triviale Projekt benötigt eine klar definierte zyklenfreie logische Architektur, die zumindest die Schichten und wenn möglich auch die fachlichen Komponenten der Anwendung und die erlaubten Abhängigkeiten zwischen diesen definiert. Der Anwendungs-Code muss diese Struktur ohne Verletzungen reflektieren (Verletzung = regelwidrige Abhängigkeit).
  2. Zyklen zwischen Packages sind nicht erlaubt (Zyklen auf höheren Ebenen werden durch Regel 1 ausgeschlossen).
  3. Der Kopplungsgrad ist niedrig zu halten (siehe Abschnitt über rACD).
  4. Codezeilen in einer Übersetzungseinheit
  5. Zyklomatische Komplexität von Methoden

Das Schöne an diesen wenigen Regeln ist, dass man bei einer weitgehenden Einhaltung sicher sein kann, ein Projekt mit weit überdurchschnittlicher technischer Qualität vor sich zu haben. Deshalb eignen sich diese Regeln auch ausgezeichnet für die Auftraggeber von Softwareprojekten, die nicht nur die Funktionalität, sondern auch die technische Qualität und damit die Wartbarkeit einer in Auftrag gegebenen Anwendung sicherstellen ­möchten.

Schlussbemerkung

Nun bleibt allerdings die spannende Frage, wie man diese Regeln denn effizient überprüfen kann. Eine Überprüfung durch regelmäßige Code-Reviews scheidet wohl allein aufgrund des hohen Aufwandes aus. Außerdem wird selbst ein noch so gewissenhafter Prüfer immer einige Probleme übersehen.

Es bietet sich also eine toolgestützte Überwachung an. Bei den Open-­Source-Werkzeugen denkt man hier zuerst einmal an das relativ populäre JDepend. Dieses findet Package-Zyklen, insofern sie nicht auf sog. Inline Dependencies beruhen. (JDepend untersucht nur den Java-Bytecode, um Abhängigkeiten zu analysieren.) Das Problem bei diesem Tool liegt darin, dass man keine Architekturvorgabe definieren kann und man daher gezwungen ist, alle Package Dependencies manuell zu überprüfen. Bei größeren Systemen ist dies ein Ding der Unmöglichkeit, also scheidet JDepend für unsere Zwecke aus.

Zusammenfassend lässt sich feststellen, dass Architektur-Management die Möglichkeit bietet, bei relativ geringem zusätzlichem Aufwand die Architektur und technische Qualität eines Systems effektiv vor einer Erosion zu bewahren. Da die Kosten und Aufwendungen, die durch eine erodierte Struktur und mangelnde technische Qualität entstehen, um ein Vielfaches über den Aufwendungen für das Architektur-Management liegen, ist es eigentlich erstaunlich, dass dieser Aspekt nicht schon heute in jedem nicht trivialen Projekt zum Standard gehört.

Alexander v. Zitzewitz ist Mitbegründer und Geschäftsführer der hello2morrow GmbH und verfügt über mehr als 20 Jahre Erfahrung im Bereich der objektorientierten Softwaretechnologie und Softwarearchitektur.
Kommentare

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht.