Montag, 7. Juli 2014

Zyklische Abhängigkeiten - wo liegt das Problem?

Dieser Artikel ist Teil der folgenden Serie über zyklische Abhängigkeiten. Zahlreiche Grundbegriffe, Konzepte und empirische Befunde wurden im Rahmen dieser Serie detailliert dargestellt. Im vorliegenden Artikel werde ich einige grundlegende Überlegungen anstellen, welche die Probleme, die durch zyklische Abhängigkeiten entstehen können, besser verständlich machen.

Die Serie

Einführung
Terminologie
Werkzeugunterstützung für Java
Wo liegt eigentlich das Problem? (dieser Artikel)
Einfluss auf Qualitätsmerkmale
Die Praxis
Verschiedene Erscheinungsformen
Zusammenhang mit der Pakethierarchie
Zusammenhang mit der Domäne
Einige Metriken
Durchbrechung von Zyklen
Das Prinzip

Transitive Abhängigkeiten

Zunächst möchte ich eine Betrachtungsweise für Abhängigkeiten erläutern, die es ermöglicht, den Umfang der Abhängigkeiten eines Gesamtsystems besser vergleichen zu können. Diese Vergleichbarkeit wird ein besseres Verständnis der Auswirkungen zyklischer Abhängigkeiten ermöglichen. Betrachten wir die folgende Konstellation:
In diesem Diagramm zählen wir zunächst zwei direkte Abhängigkeiten, nämlich A -> B und B -> C, repräsentiert jeweils durch einen Pfeil. Dies könnte dazu verleiten, dass sich die Zahl der Abhängigkeiten in einen gegebenen System einfach durch Aufsummieren der Einzelabhängigkeiten ermitteln lässt. Dies ließe aber außer Acht, dass A indirekt auch von C abhängt. Der folgende Vergleich macht dies deutlich:
In beiden Fällen zählen wir drei Elemente und zwei direkte Abhängigkeiten. Zählen wir aber die Anzahl der Elemente, von denen andere Elemente abhängen, so gelangen wir in Abbildung 1a auf drei Elemente und in Abbildung 1b lediglich auf zwei. Wir betrachten hierzu für jedes Einzelelement, wie viele Elemente von ihm aus erreicht werden können, und summieren diese Anzahlen auf. Von A aus lassen sich B und C erreichen, also zwei Elemente. Von B aus lässt sich zusätzlich C erreichen, so dass wir insgesamt zur Summe drei gelangen.

Die vorgeschlagene Zählweise berücksichtigt also auch transitive Abhängigkeiten und enthält daher mehr Information als die reine Aufsummierung der Einzelabhängigkeiten. Sie geht meines Wissens auf John Lakos (siehe [Lakos1996]) zurück, der sie zur Beurteilung der Abhängigkeiten zwischen Components (für ihn in C++ eine Kombination aus .h- und .cpp-Datei) in Packages einführt und als Cumulative Component Dependency (CCD) bezeichnet. H. Melton und E. Tempero verwenden in [Melton2007] eine auf Java-Klassen bezogene Variante, um zyklische Abhängigkeiten zwischen Klassen zu untersuchen. Sie bezeichnen diesen Wert als Class Reachability Set Size (CRSS), was sich im Deutschen am besten als "Anzahl der erreichbaren Klassen" übersetzen lässt. Im Unterschied zu Lakos' CDD bezieht sich die CRSS auf eine einzelne Klasse.

Für die folgende Betrachtung werde ich die beschriebene Zählweise nicht exklusiv auf Klassen oder andere konkrete Entitäten, sondern auf beliebige Software-Elemente beziehen, die einen Abhängigkeitsgraphen bilden können. Ich verwende dabei analog zu Lakos die Summe über alle Elemente des Systems. Diese angepasste Metrik bezeichne ich im Folgenden kurz als Reachability.

Zyklen als spezielle transitive Abhängigkeiten

Wenden wir die beschriebene Zählweise nun auf zyklische Abhängigkeiten an und betrachten den folgenden kleinen Zyklus dreier Elemente und eine "durchbrochene" Variante.
Für das Beispiel in 2a nehmen wir an, dass es sich bei den Elementen um Klassen handelt. In Abbildung 2b wurde der Klassen-Zyklus aus 2a per Dependency Inversion durchbrochen. C verwendet nun das Interface I, das von A implementiert wird. Würde es sich in 2a nicht um Klassen handeln, sondern um Pakete oder physische Einheiten, so würde sich ein etwas anderes Durchbrechungs-Muster ergeben. Wir möchten nun diese beiden Varianten hinsichtlich der Anzahl ihrer Elemente und Abhängigkeiten vergleichen. Dabei ergibt sich Folgendes:
  • In der zyklischen Variante existieren drei Elemente und drei Abhängigkeiten, in der azyklischen Version vier Elemente und vier Abhängigkeiten. 
    • Die Gesamtzahl der Elemente und Einzelabhängigkeiten ist in der Lösung 2b also um eins erhöht.
    • Das Verhältnis der Anzahl der Elemente zur Anzahl der Abhängigkeiten ist gleich geblieben. 
  • Bei Berücksichtigung der transitiven Abhängigkeiten hat sich die Situation in 2b jedoch bereits etwas verbessert. In Abbildung 2a sind von jedem Element aus zwei andere Elemente erreichbar, also insgesamt sechs Elemente. In Abbildung 2b sind von A und C aus jeweils zwei Elemente erreichbar, von B aus nur ein Element, also insgesamt fünf Elemente - immerhin eines weniger.
Betrachten wir ein etwas komplexeres Beispiel, das diesen Effekt noch deutlicher macht:
In Abbildung 3a haben wir es mit einem Zyklus in Hub-Struktur gemäß [Dietrich2014] zu tun. Jedes der sieben Elemente kann jedes andere erreichen, so dass sich eine Reachability von 7 * 6 = 42 ergibt. Als Durchbrechungs-Muster verwenden wir in 3b wiederum Dependency Inversion, gehen also für das Beispiel davon aus, dass es sich bei den Elementen in 3a um Klassen handelt. Da der Hub nicht durch ein einzelnes Interface durchbrochen werden kann, steigt die Zahl der beteiligten Einzelelemente und Einzelabhängigkeiten durch die Einführung von Dependency Inversion in Abbildung 3b recht deutlich: Wir verzeichnen nun 13 statt 7 Elemente und 18 statt 12 Einzelabhängigkeiten. Da es jedoch keine transitiven Abhängigkeiten mehr gibt, bleibt auch die Reachability bei 18 - sie wurde mehr als halbiert.

Das Beispiel legt auch nahe, dass die Reachability mit steigender Zahl der Elemente stärker ansteigt als die Anzahl der Elemente und Einzelabhängigkeiten, die für die Durchbrechung eingeführt werden müssen. Dies erklärt sich graphentheoretisch wie folgt:
  • Definitionsgemäß steht in einem Zyklus jedes Element mit jedem anderen Element zumindest transitiv in Beziehung. Die Zahl dieser Beziehungen berechnet sich mit der Formel n * (n - 1), so dass die Reachability in einem Zyklus immer quadratisch ansteigt.
  • Außer in einer sogenannten "Clique", in der jedes Element eines Zyklus mit jedem anderen Element einen direkten Teilzyklus eingeht, ist die Zahl der zu durchbrechenden Teilzyklen jedoch immer kleiner als n * (n - 1). Wie groß sie genau ist, hängt von der Anzahl der Teilzyklen im Gesamtzyklus ab, die durchbrochen werden müssen. Geht man zusätzlich davon aus, dass die Kosten für eine einzelne Durchbrechung konstant sind, wie es bei Dependency Inversion der Fall ist, so steigen die Größenmaße außer für Cliquen in jedem Fall schwächer an als die Reachability.

Wo liegt also das Problem?

Die obigen Betrachtungen zeigen, dass insbesondere bei größeren Zyklen die Reachability innerhalb des Zyklus gegenüber derjenigen einer äquivalenten zyklenfreien Struktur stark ansteigt. Dies resultiert in den folgenden beiden Problemgruppen:
  • Infizierbarkeit: Negative Eigenschaften oder erforderliche Änderungen einzelner Elemente können eine größere Menge anderer Elemente beeinflussen. Beispiele für negative Eigenschaften sind Fehleranfälligkeit, hohe Änderungsraten, Inflexibilität, schlechte Laufzeit usw. Beispiele für erforderliche Änderungen sind modifizierte Methodensignaturen oder eine geänderte Semantik einzelner Methoden.
  • Schwergewichtigkeit: Es wird schwieriger, Entwicklungsaktivitäten auf kleinere, ausgewählte Gruppen von Elementen zu beziehen. Die Element-Gruppen, mit denen jeweils umgegangen werden muss, werden somit tendentiell größer und schwerer handhabbar. Beispiele für solche Entwicklungsaktivitäten sind Verstehen, Test, Änderung, Wiederverwendung, Build usw.
Während die Infizierbarkeit die negativen Auswirkungen von Zyklen hinsichtlich der Effekte problembehafteter Einzelelemente hervorhebt, stellt die Schwergewichtigkeit auf den Einfluss von Zyklen auf die Entwicklungsprozesse selbst ab. Sowohl Infizierbarkeit als auch Schwergewichtigkeit erhöhen tendentiell die Kosten für die weitere Lebenszeit des Systems, da die Aufwände für die Pflege und Weiterentwicklung des Systems größer werden.

Quellen

[Dietrich2014] - On The Shape of Circular Dependencies in Java Programs, Al-Mutawa H., Dietrich J., Marsland S., McCartin, C., Proceedings ASWEC'14, Preprint (2014)

[Lakos1996] - Large-Scale C++ Software Design, John Lakos (1996)

[Melton2007] - The CRSS metric for package design quality, H. Melton, E. Tempero, ACSC '07 Proceedings of the thirtieth Australasian conference on Computer science - Volume 62 (2007), siehe https://www.cs.auckland.ac.nz/~hayden/MeltonTemperoISESE06.pdf