Dienstag, 22. Juli 2014

Zyklische Abhängigkeiten in der Domäne

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 behandle ich einige Aspekte sogenannter "domänenbedingter" Zyklen.

Die Serie

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

John Lakos beschreibt in [Lakos1996, S. 213-215] eine Form zyklischer Abhängigkeiten, die anscheinend durch die Domäne bedingt ist. Dabei treten zwischen Entitäten des Problemraums wechselseitige Abhängigkeiten auf, die sich in entsprechenden zyklischen Abhängigkeiten des Lösungsraums (also der Software-Elemente) widerspiegeln. Diese Zyklen erscheinen gewissermaßen als "notwendig", da keine andere zyklenfreie Lösung naheliegt. Lakos bezeichnet diesen Zyklentyp als intrinsic interdependency. Er drückt damit aus, dass die zyklische Struktur den modellierten Gegenständen des Problemraums bereits innewohnt und nicht durch Elemente außerhalb dieses Raums oder durch mangelhafte Modellierung in sie eingebracht wurde. [Melton2006] greift diese Begrifflichkeit auf und spricht von "intrinsic interdependencies between particular classes in a domain" bzw. von "intrinsic interdependencies between objects in the problem domain". Ich werde daher in diesem Zusammenhang von "domänenbedingten" zyklischen Abhängigkeiten sprechen.

Das Graph-Beispiel

Lakos untermauert die Existenz dieses Abhängigkeitstyps mit dem Beispiel eines mathematischen Graphen. Ein solcher Graph lässt sich durch zwei Klassen modellieren: Knoten (Klasse Node) und Kanten (Klasse Edge). Jeder dieser Klassen weise Eigenschaften auf, die sich nur auf sie selbst beziehen, wie z. B. der Name eines Knotens oder das Gewicht einer Kante. Aber jede dieser Klassen sollte, um die Graphenstruktur als Ganzes brauchbar zu machen, auch Informationen über die jeweils andere Klasse enthalten. Eine Kante sollte wissen, mit welchen Knoten sie verknüpft ist. Ein Knoten sollte wissen, welche Kanten ihn mit dem Rest des Graphen verbinden. Durch dieses wechselseitige Wissen entsteht der folgende simple Zyklus:
Lakos macht in der Folge allerdings diverse Vorschläge, wie sich auch solche scheinbar unvermeidbaren Zyklen auflösen lassen. Die Behandlung all dieser Möglichkeiten (wie Escalation, Demotion, Dumb Data, Manager Class u. a.) bleibt einem späteren Blog-Artikel vorbehalten. An dieser Stelle sollen zunächst einige Ergebnisse der vorherigen Blogartikel herangezogen werden, um eine Situation wie die oben gezeigte zu bewerten:
  • Wie bei der Behandlung verschiedener Zyklus-Formen gezeigt wurde, entfällt ca. die Hälfte aller Klassen-Zyklen in realen Software-Systemen auf direkte Zyklen zwischen zwei Klassen.
  • Zudem kommt diese Zyklus-Form besonders häufig zwischen Toplevel-Klassen und ihren inneren Klassen vor.
  • Der Anteil der an solchen Zyklen involvierten Klassen ist allerdings vergleichsweise gering. Er liegt bei ca. 10% aller in Zyklen involvierten Klassen.
  • Die geringe Größe solcher Zyklen sowie ihre Symmetrie machen es unwahrscheinlicher, dass die negativen Auswirkungen der Infizierbarkeit und Schwergewichtigkeit in diesen Fällen tatsächlich zu Tage treten.
Nichtsdestotrotz wäre es wünschenswert zu wissen, wie hoch der Anteil domänenbedingter Zyklen an der Gesamtzahl der Zyklen in modernen Software-Systemen tatsächlich ist. Nicht alle Zweier-Zyklen müssen domänenbedingt sein. Umgekehrt können auch größere Zyklen ihre Ursache in der Domäne haben. Ein Versuch zur Formalisierung dieser Fragestellung wurde in [Melton2006] unternommen. Diese Ergebnisse werden im folgenden Abschnitt zusammengefasst.

Formalisierung domänenbedingter Zyklen

Lakos unterscheidet u. a. folgende Nutzungsbeziehungen:
  • Verwendung im Interface (Uses-In-The-Interface): Ein Typ wird bereits bei der Deklaration einer Methode (also in ihrer Signatur) bzw. zur Deklaration nicht-privater Felder der Klasse verwendet.
  • Verwendung in der Implementierung (Uses-In-The-Implementation): Ein Typ wird innerhalb der Implementierung einer Methode verwendet.
In [Melton2006] wird nun argumentiert, dass die Zahl der gefundenen Zyklen, in die eine Verwendung im Interface involviert ist, als Obergrenze Anzahl der potentiell im System vorhandenen domänenbedingten Zyklen gewertet werden kann. Die Autoren folgen damit der folgenden Aussage von Lakos im Kontext der beschriebenen intrinsic interdependencies:
"Inherent coupling in the interface of related abstractions makes them more resistant to hierarchichal decomposition." [Lakos1996, S. 213]
Ich werde auf dieses Argument unten im Abschnitt "Stimmt Lakos' Argument?" zurückkommen.

Erstaunlicherweise ist der Anteil der Verwendungen im Interface sehr gering. In der folgenden Abbildung sind links die Häufigkeiten der Verwendungen in der Implementierung dargestellt, rechts die Häufigkeiten der Verwendung im Interface, wobei die farbigen Teilbalken jeweils die Anzahl von Zyklen verschiedener Größen widerspiegeln.
Aus diesen Ergebnissen geht hervor, dass Zyklen, die durch die Verwendung im Interface bedingt sind, sehr viel kleiner sind als Zyklen, die durch die Verwendung in der Implementierung bedingt sind. Folgt man der oben beschriebenen Argumentation, so bedeutet dies, dass die meisten Klassen unnötigerweise in Zyklen involviert sind. Die Domäne kann nach dieser Argumentation nur für eine Minderzahl von Klassen als Rechtfertigungsgrund für Zyklen herangezogen werden.

Stimmt Lakos' Argument? 

Um zu beurteilen, ob die beschriebenen Studienergebnisse für domänenbedingte Abhängigkeiten tatsächlich Relevanz haben, wenden wir uns nochmals Lakos' Argumentation zu, dass es insbesondere die "Koppelungen im Interface der zusammenhängenden Abstraktionen" [Lakos1996, S. 213] sind, die sich einer hierarchischen, also zyklenfreien Struktur widersetzen.

Lakos' Argument lässt sich wie folgt verstehen:
  • Abhängigkeiten in der Domäne treten bevorzugt in Form von Abhängigkeiten zwischen den beteiligten Entitäten (Lakos: "related abstractions") der Domäne auf.
  • Dieser Zusammenhang manifestiert sich insbesondere in der öffentlichen Schnittstelle dieser Entitäten.
Bedauerlicherweise führt Lakos nicht näher aus, warum dies so sein soll. Im Fall der Nutzung per Schnittstelle erhält ein Nutzer die benötigte Instanz per Methodenparameter oder liefert sie per Rückgabetyp zurück. Im Fall der ausschließlichen Benutzung in der Implementierung erzeugt er die Instanz entweder selbst oder besorgt sie sich per Methodenaufruf. Es ist nicht offensichtlich, warum domänenbedingte Abhängigkeiten besonders häufig den Weg über die Schnittstelle bedingten sollten.

Eine denkbare Argumentation könnte wie folgt lauten: Die Entitäten eines Problemraums sollten innerhalb des Software-Systems aus Gründen der Datenintegrität häufig nur genau ein Mal instanziiert werden, da sie auch im Problemraum nur genau ein Mal existieren. Wird dieser Regel gefolgt, so bedeutet dies, dass die Instanzen, wenn sie von unterschiedlichen Nutzern benötigt werden, jeweils über die Schnittstellen dieser Nutzer "weitergereicht" werden müssen. 

Genau diese Situation lässt sich auch am gegebenen Graph-Beispiel nachvollziehen: Im Problemraum des Graphen existiert jeder Knoten und jede Kante nur genau ein einziges Mal. Würde jeder Nutzer des Graphen eigene Instanzen dieser Knoten und Kanten erzeugen, so würde dies nicht nur zu einem übermäßigen Speicherverbrauch führen, sondern es bestünde auch die Gefahr von Inkonsistenzen bei Modifikationen dieser Instanzen, falls diese nicht eigens durch transaktionale Mechanismen geschützt sind. Es ist daher sinnvoll, alle Instanzen nur genau ein einziges Mal zu instanziieren und allen Benutzern bei Bedarf per Referenz zur Verfügung zu stellen. Diese Referenzen drücken sich als Verwendungen im Interface aus.

Auch wenn diese Argumentation plausibel ist, so ist sie doch nicht empirisch bestätigt, so dass in der beschriebenen Formalisierung domänenbedingter Abhängigkeiten eine gewisse Gefahr für die Validität der Ergebnisse liegt. Aus meiner Sicht kommen auch andere Konstellationen als "domänenbedingt" in Betracht - z. B. die besonders häufig in Zyklen auftretenden Beziehungen zwischen Toplevel-Klassen und inneren Klassen. Diese hatten wir jedoch bereits an anderer Stelle als weitgehend unproblematisch eingestuft.

Schlussfolgerung

Ich gehe für diese Schlussfolgerung einmal davon aus, dass sich domänenbedingte Abhängigkeiten tatsächlich primär im Interface beteiligter Entitäten wiederfinden. Diese Prämisse ist bei der Untersuchung konkreter Systeme ggf. zu überprüfen, da sie im Einzelfall nicht gegeben sein kann.

Die oben gezeigten Studienergebnisse zeigen dann, dass Zyklen, die bereits in der Domäne begründet sind, nur einen kleinen Teil der in Zyklen involvierten Klassen betreffen. Domänenbedingte Zyklen tendieren auch dazu, deutlich kleiner zu sein als unnötige Zyklen. Es sollte daher sehr gut abgewogen werden, ob ein analysierter Zyklus tatsächlich als "domänenbedingt" eingestuft und damit akzeptiert wird. Insbesondere wenn dieses Rechtfertigungsmuster besonders häufig angewandt wird oder wenn größere Zyklen so begründet werden, sind Zweifel angebracht.

Quellen

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

[Melton2006] - An Empirical Study of Cycles among Classes in Java,  H. Melton, E. Tempero (2006)