Montag, 22. September 2014

Zyklische Abhängigkeiten durchbrechen

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 stelle ich einige grundlegende Vorgehensweisen zur Durchbrechnung von Zyklen dar.

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
Einige Metriken
Durchbrechung von Zyklen (dieser Artikel)
Das Prinzip

In den vorausgehenden Artikeln wurden die Erscheinungsformen zyklischer Abhängigkeiten sowie einige Hilfsmittel zur Bewertung ihrer potentiellen Schädlichkeit beschrieben. Je nach der Verfügbarkeit statischer Analysewerkzeuge kann auf dieser Grundlage ein Regelwerk aufgebaut werden, das zur Bestimmung derjenigen Zyklen einer Software führt, die vorrangig beseitigt werden sollten. Wir gehen nun davon aus, dass die zu beseitigenden Zyklen ermittelt wurden und besprechen einige grundlegende Aspekte, die bei der Durchbrechung von Zyklen eine Rolle spielen. Diese Aspekte lassen sich in zwei Gruppen unterteilen:
  • Elementare Mittel zur Beseitigung von Abhängigkeiten
  • Lösungsmuster, die sich speziell auf die Beseitigung von Zyklen beziehen
Das ganze Feld der Durchbrechung von Abhängigkeiten und Zyklen ist allerdings so vielschichtig, dass die Behandlung dieser Aspekte allenfalls ein erstes hilfreiches Handwerkszeug bereitstellen kann. Je nach Anzahl und Umfang der zu beseitigenden Zyklen wird es erforderlich sein, Strategien zu entwickeln, wie die Restrukturierungsmaßnahmen erfolgen sollen. Auf solche übergreifenden Strategien (wie z. B. den Einsatz spezieller Visualisierungen [Laval2010], die Spezifikation von und Orientierung an Schichten [Vainsencher2004] oder gar ein automatisiertes Verschieben von Klassen zur Beseitigung von Paket-Zyklen [Shah2012]) wird hier nicht näher eingegangen.

Elementare Mittel zur Beseitigung von Abhängigkeiten

In diesem Abschnitt werden wir im Einklang mit der Arbeit [Shah2013] zwei Ebenen unterscheiden:
  • Atomare Refactorings, die oftmals werkzeuggestützt in einem einzigen Arbeitsvorgang ausgeführt werden können. Zu diesen gehören insbesondere zahlreiche Refactorings, die bereits in [Fowler1999] zusammengetragen wurden, wie z. B. das Verschieben von Methoden oder Klassen oder das Extrahieren von Interfaces.
  • Zusammengesetzte Refactorings, welche mehrere primitive Refactorings kombinieren, um eine übergeordnete Lösungsidee zu erreichen. Die Lösungsidee besteht dabei oft in einem Entwurfsmuster oder ähnelt einem solchen. Beispielsweise ist es zur Umsetzung einer Dependency Inversion oftmals erforderlich, mehrere primitive Operationen durchzuführen, bis die Gesamtidee umgesetzt ist.
Zudem ist zu unterscheiden, auf welcher Ebene durch das jeweilige Refactoring eine Abhängigkeit beseitigt wird: auf Typ-Ebene, Paket-Ebene oder auf der Ebene physischer Einheiten. Da sich Abhängigkeiten auf der Ebene phyischer Einheiten in der Regel durch entsprechende Build-Skripte ergeben, gehe ich auf diese hier nicht speziell ein. Zur Orientierung können die Refactorings der Paket-Ebene dienen.

Einige atomare Refactorings durchbrechen Abhängigkeiten direkt. Beispielsweise kann das "Move Class"-Refactoring eine Abhängigkeit auf Paket-Ebene direkt durchbrechen, falls die verschobene Klasse die einzige Ursache dieser Abhängigkeit ist. Andere atomare Refactorings können nur im Rahmen eines zusammengesetzten Refactorings zu einer Durchbrechung beitragen. Beispielsweise bewirkt die Anwendung von "Extract Interface" für sich genommen noch keine Entkoppelung. Erst in Zusammenhang mit "Type Generalization", also der tatsächlichen Verwendung des Interface-Typs in den Deklarationen eines Benutzers, trägt das extrahierte Interface zur Durchbrechung von Abhängigkeiten bei.

Zu den wichtigsten atomaren Refactorings, die zur direkten Beseitigung von Abhängigkeiten verwendet werden können, gehören:
  • Refactorings der Paket-Ebene (auch anwendbar auf die Ebene der physischen Einheiten):
    • Move Class: Das Verschieben einer Klasse in ein anderes Paket.
    • Split Package: Das Aufteilen eines Pakets in zwei oder mehr Pakete.
    • Merge Package: Das Vereinigen von zwei oder mehr Paketen zu einem Paket.
  • Refactorings der Klassen-Ebene:
    • Move Method/Field: Das Verschieben einer Methode oder eines Feldes in eine andere Klasse. Ggf. auch innerhalb der Typhierarchie, dann auch "Pull Up Method/Field" oder "Push Down Method/Field" genannt.
    • Extract Delegate: Verwandt mit "Move Method/Field", wobei eine oder mehrere Methoden oder Felder in eine neue Klasse verschoben werden.
    • Merge Classes: Das Vereinigen zweier Klassen zu einer gemeinsamen Klasse, z. B. auch wenn eine Super- und ihre Subklasse sich nicht wesentlich unterscheiden (in diesem Fall auch "Collapse Hierarchy" genannt).
    • Inline Class/Method: Triviale Klassen oder Methoden können beseitigt und ihre triviale Funktionalität vom Benutzer direkt umgesetzt werden. Dies geht oftmals mit einer gewissen Redundanz einher, die auf Grund der Trivialität der Funktionalität ggf. eher akzeptiert werden kann als die Abhängigkeit.
    • Type Generalization/Use Interface where possible:  Existiert bereits ein Interface-Typ zu einem Implementierungs-Typ, so kann die Entkopplung eines Nutzers der Implementierung durch Umdeklaration erfolgen. Ggf. kann ein "Extract Interface"- oder "Extract Superclass" vorgeschaltet werden.
Einige zusammengesetzte Refactorings, die bei der Beseitigung von Abhängigkeiten eingesetzt werden können, sind folgende:
  • Dependency Inversion: Die Standardlösung zur Umsetzung einer Dependency Inversion basiert auf der Einführung eines abstrakten Supertyps für eine konkrete Klasse (z. B. mittels "Extract Interface") sowie der Nutzung dieses Supertyps in den benutzenden Klassen ("Type Generalization", siehe oben). Gelegentlich verbleibt in solchen Fällen jedoch noch das Erzeugungsproblem: Wie gelangt der Benutzer an eine konkrete Instanz, ohne den Konstruktor der konkreten Implementierung aufzurufen (womit die Abhängigkeit fortbestehen würde)? Zur Lösung des Erzeugungsproblems existieren diverse Entwurfsmuster wie z. B. die Einführung einer Factory oder eines Builders. Eine weitere Lösung besteht in der Nutzung eines Dependency Injection-Frameworks.
  • Nutzung von Abstraktionen: Die meisten Entwurfsmuster nutzen Abstraktionen, durch die in vielen Fällen Entkopplungen erreicht werden. Auch die eben beschriebene Dependency Inversion basiert auf der Nutzung von Abstraktionen. Abstraktionen können als eine sehr allgemeine Möglichkeit zur Durchbrechung von Abhängigkeiten zwischen konkreten Implementierungen angesehen werden. Es ist freilich im Einzelfall einige Erfahrung erforderlich, um die Anwendbarkeit eines konkreten Musters zu erkennen.

Lösungsmuster zur Durchbrechung von Zyklen

Um Zyklen zu durchbrechen, müssen Einzelabhängigkeiten durchbrochen werden. Daher haben wir die Durchbrechung von Einzelabhängigkeiten im vorherigen Abschnitt näher betrachtet. Insbesondere John Lakos [Lakos1996] hat jedoch einige sehr konkrete Lösungsmuster beschrieben, die sich speziell auf die Durchbrechung von Zyklen der Topologie Tiny beziehen (siehe auch Details zu Zyklus-Topologien). Zur Erinnerung werfen wir einen Blick auf die wesentlichen Erscheinungsformen von Zyklen:
Bei näherer Betrachtung fällt auf, dass die Tiny-Topologie der grundlegende Baustein für die meisten anderen Topologien ist. Die Topologien Chain und Star bestehen ausschließlich aus vielen Tiny-Zyklen, Clique und Multi-Hub bestehen überwiegend aus ihnen. Bei Semi-Cliquen kann der Anteil der Tiny-Zyklen variieren, und lediglich Circle ist frei von ihnen. Demgemäß kommt der Behebung von Tiny-Zyklen, also von Zyklen zwischen zwei Elementen, eine besondere Bedeutung zu.
Lakos beschreibt insgesamt neun Lösungsmuster. Ich werde von diesen die folgenden vier wesentlichen und allgemein nutzbaren Muster näher beschreiben. Die anderen Muster sind entweder Spezialfälle dieser vier Muster oder können nur in einem C++-Kontext eingesetzt werden:
  • Eskalation
  • Herabstufung
  • Redundanz
  • Callbacks
Eskalation
Im Rahmen einer Eskalation (Lakos: escalation) wird diejenige Funktionalität, welche die wechselseitige Abhängigkeit erzeugt, in eine übergeordnete Einheit ausgelagert. Ein Beispiel für eine solche Restrukturierung ist der Zyklus, der zwischen den Klassen Node und Edge entsteht, wenn man die Beziehungsinformation eines Graphen in diesen Klassen vorhält. Knoten kennen dann ihre verbundenen Kanten, und Kanten kennen die Knoten, an denen sie enden. Diese Beziehungsinformation kann jedoch auch von einer übergeordneten Klasse Graph verwaltet werden. Alle Operationen, die sich auf die Graphenstruktur beziehen, werden dann in dieser übergeordneten Einheit abgewickelt, während Node und Edge ausschließlich die Eigenschaften behandeln, die spezifisch für sie sind, und keine Kenntnis der jeweils anderen Klasse mehr benötigen.

Herabstufung
 Das Gegenstück zu einer Eskalation bildet die Herabstufung (Lakos: demotion). Dabei wird diejenige Funktionalität, die wechselseitig genutzt wird, in eine Dienstklasse ausgelagert, die dann von beiden ursprünglichen Klassen genutzt wird. Ggf. können auch zwei oder mehr neue Dienstklassen entstehen, am naheliegendsten ist diese Lösung jedoch, wenn nur eine einzige neue Einheit mit einer klar umrissenen Aufgabe entsteht.

Redundanz
Die Einführung einer Redundanz (Lakos: redundancy) ist uns bereits bei der obigen Behandlung elementarer Refactorings unter der Bezeichnung "Inline Class/Method" begegnet. In Einzelfällen kann es weniger schädlich sein, eine solche Redundanz in Kauf zu nehmen, als weiterhin mit dem Zyklus leben zu müssen. Je trivialer die kopierte Funktionalität ist, desto weniger schwer wiegt der Verstoß gegen Don't Repeat Yourself

Callbacks
Lakos selbst stellt Callbacks zwar als Lösungsmöglichzeit zur Beseitigung von Zyklen vor, warnt jedoch auch vor ihrem Einsatz: "Die unüberlegte Verwendung von Callbacks kann zu Entwürfen führen, die schwer zu verstehen, zu debuggen und zu warten sind" [Lakos1996]. Je nach Programmiersprache können Callbacks in unterschiedlichen Erscheinungsformen wie z. B. Zeiger auf Funktionen, First-Class-Funktionen, Event-Handler usw. auftreten. Durch die mit ihnen verbundene Inversion of Control wird die Abhängigkeit in der Regel umgekehrt.

Fazit

Wie jede Form von Restrukturierung kann die Beseitigung von Zyklen ein umfassender und komplexer Vorgang sein. Es kann jedoch zur Beruhigung beitragen, wenn man sich verdeutlicht, dass sich die meisten Operationen, die bei der Beseitigung von Zyklen eingesetzt werden, in zwei Kategorien einteilen lassen:
  • Verschieben von Elementen
  • Einführung von Abstraktionen
Freilich treten Operationen dieser Kategorien in sehr unterschiedlichen Erscheinungsformen auf, und der vorliegende Artikel kann nur einen groben Überblick der Möglichkeiten geben. Typische Verschiebemuster sind Eskalation und Herabstufung. Der Einsatz von Abstraktionen ist oftmals der schwierigere Teil der Aufgabe. Neben der Umsetzung einer Dependency Inversion, die ein häufiges Standardmittel zur Durchbrechung ist, können zahlreiche Entwurfsmuster als Leitfaden dienen. Insgesamt darf festgehalten werden, dass zur Beseitigung von Zyklen oftmals nur Schritte erforderlich sind, die zum Handwerkszeug eines jeden Entwicklers gehören sollten. Die eigentliche Kunst besteht dabei wohl eher darin, die Gesamtheit der Zyklen im Auge zu behalten und ein zyklenfreies Zielbild zu entwerfen, an dem sich die Restrukturierungsarbeit orientieren kann.

Quellen

[Fowler1999] - Refactoring: Improving the Design of Existing Code - Martin Fowler (1999)

[Laval2010] - Identifying Cycle Causes With CycleTable - Jannik Laval, Simon Denier, Stephane Ducasse (2010)

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

[Shah2012] - Making Smart Moves to Untangle Programs - Syed Muhammad Ali Shah, Jens Dietrich, Catherine McCartin (2012)

[Shah2013] - On the Automation of Dependency-Breaking Refactorings in Java - Syed Muhammad Ali Shah (2013)

[Vainsencher2004] - Mudpie: layers in the ball of mud - Daniel Vainsencher (2004)