Über unsMediaKontaktImpressum
Hendrik Lösch 20. Juni 2017

Clean Code vs. Abhängigkeiten

Trotz Clean Codes wird man sich von manchen Abhängigkeiten niemals befreien können. © Vadimsadovski / Fotolia.com
© Vadimsadovski / Fotolia.com

Abhängigkeiten sind das Salz in der Softwareentwicklungssuppe. Ohne geht es einfach nicht, zu viel macht das Gesamtwerk aber ungenießbar. Als Mittel gegen diese Problematik wird häufig das Clean Code-Development gesehen, welches zu weiten Teilen auf den Arbeiten von R. C. Martin basiert [1]. Doch was genau sind die Probleme mit Abhängigkeiten und wie zielt Clean Code-Development darauf ab?

Grundsätzlich besteht immer eine Abhängigkeit zwischen zwei Vertragspartnern, wenn ein Klient Arbeit an einen anderen Leistungsträger auslagert. Dies ist im alltäglichen Gebrauch nicht anders als in der Softwareentwicklung. Beauftragt man die Kfz-Werkstatt mit der Reparatur eines Fahrzeugs, stellt man eine Abhängigkeit zur Güte der Arbeit jener Werkstatt her. Sollte diese Güte zu wünschen übrig lassen, dann wird sich das auch auf die Güte des Fahrzeuges auswirken. Da man selbst das Fahrzeug benötigt, um damit zur Arbeit zu fahren, kann sich das Problem soweit fortpflanzen, dass eine schlecht verlaufene Reparatur Auswirkungen auf die eigene Leistungsfähigkeit hat. Beispielsweise dann, wenn das Fahrzeug auf dem Weg zur Arbeit erneut kaputtgeht.

Glücklicherweise haben wir außerhalb der Programmierung immer die Möglichkeit, Alternativen zu nutzen. Sollte eine Werkstatt keine gute Arbeit leisten, werden wir wohl nur ein einziges Mal den Fehler machen und dort eine Reparatur beauftragen. Immerhin gibt es ja sehr viele Werkstätten zu denen wir wechseln können. Sollte unser Auto regelmäßig kaputt gehen, können wir auf ein neues, ein Fahrrad oder den öffentlichen Nahverkehr wechseln. All dies ist möglich, weil wir mit unserer Sprache, Währung u. a. über einheitliche Schnittstellen verfügen, um unterschiedliche Leistungsträger zu beauftragen. Tatsächlich aber verfügen wir auf diese Weise zwar nicht über eine Abhängigkeit zu den Leistungsträgern, sehr wohl aber zu den Schnittstellen gegenüber den Leistungsträgern. Die Möglichkeit, sich gegen die Auswirkungen einer konkreten Entscheidung abzusichern, ist demnach die Abstraktion der eigentlichen Belange in eine Form, die von der Ausführung möglichst unabhängig ist.

Ob die Basistechnologie zeitgemäß, problemangemessen und langfristig stabil ist, wird vergleichsweise selten betrachtet.

Kommen wir nun aber zurück zur eigentlichen Softwareentwicklung und beginnen hier ganz grundlegend. Da wir keine Nullen und Einsen für die Programmierung nutzen wollen, müssen wir uns zuallererst einmal auf eine Basistechnologie wie Java, .Net oder PHP einigen. Diese Entscheidung und die damit verbundene Abhängigkeit von einem Ökosystem wird in sehr vielen Fällen ohne größere Überlegung getroffen. Oft genug sind die eigentlichen Gründe für die Festlegung auf eine Basistechnologie im Vorwissen der bereits vorhandenen Entwickler sowie deren persönlichen Interessenlagen zu finden. Ob die Basistechnologie zeitgemäß, problemangemessen und langfristig stabil ist, wird vergleichsweise selten betrachtet.

Was aber, wenn sie nicht mehr gepflegt wird? Was, wenn die Güte der darin enthaltenen Funktionalität sinkt und sich somit auch auf die Güte unserer eigenen Arbeit auswirkt? Was, wenn die Community sehr klein ist oder schrumpft und man somit weniger Hilfe bei dringenden Problemen bekommt? Laut unserem Eingangsbeispiel brauchen wir uns darüber keine Gedanken machen, insofern wir eine saubere Abstraktion gegenüber der Basistechnologie besitzen. Immerhin können wir sie ja dann schnell durch eine andere austauschen. Tatsächlich weiß jedoch jeder Entwickler, dass allein schon die Abhängigkeit zur Programmiersprache, die häufig selbst Teil der Basistechnologie ist, eine komplette Neuentwicklung bewirken würde, sollte man sich für eine andere Grundlage der Software entscheiden.

Daraus ergibt sich, dass es in der Softwareentwicklung Abhängigkeiten gibt, von denen man sich niemals wird befreien können. In aller Regel spricht man in diesem Fall von beständigen Abhängigkeiten. Dies können neben der Basistechnologie auch die Verwendung von Basisdatentypen wie Integer, Character oder String sein. Sie sind so elementar und allgemein, dass ihre direkte Nutzung erzwungen ist, da jeder Versuch, sie zu abstrahieren, nur darin enden würde, die gleichen Mittel für eine Abstraktion zu nutzen, die man eigentlich abstrahieren möchte.

Gegenüber den beständigen Abhängigkeiten gibt es noch den weitaus größeren Teil der unbeständigen. Hierzu zählen beispielsweise Third Party-Frameworks oder Code, der von uns selbst geschrieben wurde. Dies sind Dinge, die sich jederzeit – und bei eigenem Code sogar ohne größere Vorwarnung – ändern können. Wie schnell dies geht, sieht man beispielsweise, wenn man sich eine neue Version einer Third Party-Bibliothek herunterlädt, oder wenn man einfach nur nach zwei Wochen Urlaub einen Blick in den aktuellen Quellcodestand wirft. Gerade im letzten Fall wird man Stellen entdecken, in denen kein Stein auf dem anderen blieb.

Jede dieser Änderungen kann in ihrer Güte variieren und dabei unter Umständen negative Verhaltensänderungen in all den Bestandteilen hervorrufen, die den Code nutzen, welcher geändert wurde. Je mehr Abhängigkeiten zu finden sind, desto schwerer wiegen dabei in aller Regel die Auswirkungen einer Änderung. Basierend auf diesem Umstand hat Robert C. Martin drei Eigenschaften von schlechtem Design beschrieben [2]:

  1. Änderungen können schwer vorgenommen werden, da jede Änderung zu viele Bestandteile des Systems zeitgleich betrifft -> Starr
  2. Ändert man das System, brechen Bestandteile unerwartet, die eigentlich nicht von der Änderung hätten betroffen sein sollen -> Zerbrechlich
  3. Es ist schwer, einzelne Bestandteile erneut zu verwenden, da sie nicht aus dem Gesamtkonstrukt herausgelöst werden können -> unbeweglich.
Abb.1: Ausgehende Abhängigkeiten. © Hendrik Lösch
Abb.1: Ausgehende Abhängigkeiten. © Hendrik Lösch

In diesem Zusammenhang gibt es zwei weitere Unterscheidungsmöglichkeiten von Abhängigkeiten: eingehende und ausgehende Abhängigkeiten. Abb.1 zeigt einen Abhängigkeitsgraphen, in dem man beide Arten sehr gut erkennen kann. In ihm fließen alle Abhängigkeiten gerichtet von der Spitze zur untersten Ebene. Jeder Knoten stellt einen Codebestandteil dar und die darin enthaltene Zahl gibt an, wie viele ausgehende Abhängigkeiten der Bestandteil besitzt.

Sollte es Änderungen an einem Codebestandteil geben, die dessen Arbeitsweise negativ beeinflussen, kann sich dies negativ auf alle Bestandteile auswirken, die ihn als ausgehende Abhängigkeit besitzen. Auf der untersten Ebene hängen alle Bestandteile nur von sich selbst ab, daher enthalten sie eine Eins. Eine Ebene darüber haben die Elemente drei Abhängigkeiten, sich selbst und zwei folgende. Die oberste Ebene hat sich selbst, die darunter liegende Ebene und transitiv die darauf folgende als Abhängigkeiten und zeigt daher eine Sieben.

IT-Tage 2017 - Softwareentwicklung
Abb.2: Eingehende Abhängigkeiten. © Hendrik Lösch
Abb.2: Eingehende Abhängigkeiten. © Hendrik Lösch

Umgekehrt verhält es sich mit den eingehenden Abhängigkeiten. Sie betrachten wiederum, von wie vielen Bestandteilen ein Bestandteil genutzt wird. Für die untersten Kästchen ergibt sich somit eine Drei, weil sie von sich selbst abhängen und als Abhängigkeit der darüber liegenden Bestandteile gilt. Eingehende Abhängigkeiten beeinflussen das Verhalten eines Codebestandteils in aller Regel nicht und werden deshalb als weniger kritisch angesehen. Sie können aber beispielsweise eine sehr gute Indikation dafür sein, wo sich Änderungen an den öffentlichen Schnittstellen als Breaking Changes auswirken.

Abb.3: Kreisabhängigkeiten. © Hendrik Lösch
Abb.3: Kreisabhängigkeiten. © Hendrik Lösch

In Kombination beeinflusst somit das Verhältnis zwischen eingehenden und ausgehenden Abhängigkeiten aller Bestandteile die Stabilität des Gesamtsystems und dies umso mehr, je häufiger wir im Code Kreisabhängigkeit finden. Kreisabhängigkeiten sorgen dafür, dass ein System sich selbst als Abhängigkeit hat. Auf diese Weise kann sich im Grunde jede Änderung auf nahezu jeden Bestandteil auswirken, da sich eingehende Abhängigkeiten plötzlich in ausgehende wandeln. Dies wird umso deutlicher, wenn man sich den kumulierten Abhängigkeitsgrad des Beispielgraphen ansieht. Ist dieser in der ersten Abb. 4 * 1 + 2 * 3 + 7 = 17 kommen wir bei Abb.3 auf 7² = 49 und somit auf fast das Dreifache an Abhängigkeiten.

Das Ziel sollte also sein, vor allem bestimmte Schlüsselstellen so zu abstrahieren, dass der Abhängigkeitsgraph geordnet wird und Teilbereiche austauschbar werden. Austauschbar bedeutet in diesem Zusammenhang nicht zwangsläufig eine Wiederverwendung von Code, sondern in der Praxis meist eher ein Entfernen und Ersetzen durch neuen Code mit dem die Anforderungen besser erfüllt werden können. Man könnte im Grunde sogar die Anpassung von vorhandenem Code als einen Austausch verstehen, da ja Code mit einer bestimmten Struktur durch Code mit einer anderen Struktur getauscht wird.

Um diesen Austausch zu erlauben, wird eine spezifische Schnittstelle definiert, die nur den Teil abstrahiert, der im konkreten Fall benötigt wird. Wie dieser Teil dann konkret umgesetzt wird, kann variieren. Das bedeutet zur Laufzeit im ungünstigen Fall erneut eine Kreisabhängigkeit aber sobald sie sich als tatsächlich schädlich herausstellt, kann man die Schnittstelle anderweitig implementieren und somit den Kreis durchbrechen.

Die Grundlage dafür ist aber, dass die Schnittstellen nicht von der Implementierung abhängig sind. Bezugnehmend auf das Eingangsbeispiel könnte man bei der Suche nach einer neuen Werkstatt davon ausgehen, dass einem selbst egal ist, welche man beauftragt, insofern es eine VW-Vertragswerkstatt ist die maximal fünf Kilometer vom eigenen zuhause entfernt ist. Diese Bedingungen scheinen sich von der ursprünglichen Implementierung zu lösen. Da es Umkreis von fünf Kilometern aber nur sehr wenige und noch weniger Vertragswerkstätten geben dürfte, führen sie dennoch dazu, dass nur eine einzige Werkstatt in Frage kommt. Man hat also nicht genug abstrahiert und somit eigentlich keinen Mehrwert für sich selbst geschaffen.

In der Programmierung passiert dies häufig, wenn Schnittstellen aus der aktuellen Implementierung heraus generiert und nicht anhand der eigentlichen Belange gestaltet werden. Haben wir beispielsweise eine Klasse KfzMaintenanceManagement, die die Verwaltung der aktuellen Wartungsaufträge enthält und dabei "praktischerweise" auch eine Methode zur Erstellung der Rechnungen, so bringt es keine entscheidenden Vorteile, eine Schnittstelle IKfzMaintenanceManagement zu generieren, die der Klasse KfzMaintenanceManagement 1:1 gleicht, da Codebestandteilen, die eigentlich nur an der Rechnungslegung interessiert sind, auch gänzlich andere Dinge aufgezwungen werden.

Die Vermischung von Belangen, Rechnung und Auftragsverwaltung, die bei der Implementierung evtl. Sinn macht, sorgt auf der Ebene der Klienten für übermäßige Abhängigkeiten, in dem ihnen schlicht zu viele Informationen bereitgestellt werden. Auf diese Weise kann sich unter Umständen eine Änderung in der Auftragsverwaltung auch direkt auf die Rechnungen auswirken.

In diesem Zusammenhang wird gern auch von Headerinterfaces gesprochen. Dabei handelt es sich um Schnittstellendefinitionen, die 1:1 den Aufbau einer spezifischen Klasse widerspiegeln, ähnlich wie es die Headerdateien in C++ tun. Dem gegenüber spricht sich das Clean Code-Development für schlanke, Anwendungsfallbezogene Rolleninterfaces aus.

Damit sind wir auch endlich beim Clean Code angekommen. Dessen Hauptziele sind:

  1. Verständlichkeit fördern
  2. Komplexität gering halten
  3. Austauschbarkeit erlauben

Betrachtet man also das Design von Schnittstellen und versucht sich gegen allzu viele Abhängigkeiten zu schützen, sollte man sich an Clean Code-Prinzipien wie das Information Hiding halten. Dieses sagt aus, dass einem Klienten nur so viel Informationen über die eigentliche Umsetzung bereitstehen sollten, wie notwendig sind, um die Umsetzung anzufordern. Dies spiegelt sich auf der anderen Seite im Interface Segregation Principle wider, durch welches wenige große Schnittstellen in viele kleine, Anwendungsfall bezogene aufgespalten werden. In Kombination mit dem Dependency Inversion Principle werden diese Schnittstellen vom Klienten und nicht vom Leistungsträger definiert, wodurch sie von High-Level-Informationen und nicht von Implementierungsdetails abhängen. Man kommt auf diese Weise dauerhaft zu einer Architektur die aus vielen kleinen Elementen, statt wenigen großen bestehen. Diese kleinen Bestandteile werden dann gezielt an einzelnen Stellen aggregiert, wodurch sich die eigentliche Anwendungslogik ergibt.

Bezogen auf unser KfzMaintenanceManagement ergibt sich somit für die Abrechnung zunächst eine Schnittstelle IInvoiceCalculation mit einer Methode CalculateInvoiceWithVAT, um die Abrechnung mit Mehrwertsteueranteil umzusetzen. Welcher Mehrwertsteuersatz zu nutzen ist, kann von der Berechnungslogik selbst entschieden werden bzw. wird von eigenständigen Klassen ermittelt, die wiederum von der IInvoiceCalculation aggregiert werden. Diesen Teil kann man anschließend aus dem KfzMaintenanceManagement herauslösen, der für sich genommen schon allein vom Namen her erahnen lässt, dass er zu viele Belange zeitgleich abdeckt. Auf diese Weise bestehen dann auch in der Software nur noch dort Abhängigkeiten zur Kfz-Wartungslogik, wo sie gebraucht werden. Braucht man tatsächlich die Logik zur Erstellung einer Abrechnung, kann man diese nun nutzen, ohne sich zeitgleich von allzu vielen Dingen abhängig zu machen.

Dies sind bei weitem nicht die einzigen Clean Code-Regeln und sollen nur eine grundsätzliche Erläuterung darstellen. Im ursprünglichen Buch von Robert C. Martin finden sich noch wesentlich mehr Informationen, weshalb es auch zur Pflichtlektüre eines jeden Programmierers zählt, der objektorientierte Technologien nutzt.

Abschließend stellt sich noch die Frage, wie viel Abstraktion genug ist und wo man letztendlich aufhören sollte. Man muss Abhängigkeiten "scopebezogen" als fest oder variabel betrachten. Je nachdem, wie fest oder variabel der Bestandteil ist, der sich im Scope der Entscheidung befindet, so beständig oder unbeständig darf dann auch die Abhängigkeit sein. In aller Regel leben Abstraktionen und Schnittstellendefinitionen länger als ihre tatsächlichen Implementierungen. So macht beispielsweise eine Abstraktion gegenüber bestimmten Features in einem Datenbankmanagement-System wenig Sinn, wenn man sich selbst damit möglicher Produktivitätssteigerung beraubt. Der Austausch einer Datenbanktechnologie geschieht äußerst selten und kann demnach als beständig angesehen werden. Der direkte Zugriff auf bestimmte Tabellen aus verschiedenen Bereichen des Quellcodes heraus sollte jedoch bspw. über das Repository Pattern [3] vereinheitlicht und abstrahiert werden, damit die Anwendungslogik nicht zwangsläufig von Änderungen in der Datenbank betroffen ist.

Einen besonderen Fall stellt die Domänenlogik einer Anwendung dar. Diese ändert sich in vielen Fällen eher selten und sollte nicht davon abhängen, welche Benutzerschnittstelle oder Datenspeicher man nutzt. Sie ist der Kern der Anwendung und muss daher so gestaltet werden, dass sie möglichst wenige ausgehende Abhängigkeiten besitzt, damit sie dauerhaft weiterverwendet werden kann. Einzige Ausnahme in diesem Zusammenhang stellt wiederum das Domänenmodell dar. Jenes beschreibt die Datenstruktur einer Domäne und zieht sich zumindest in ihren Grundstrukturen auf Dauer durch die gesamte Applikation inkl. diverser Drittsysteme wie Datenbanken usw.

Abb.4: Clean Architecture nach Robert C. Martin. © Hendrik Lösch
Abb.4: Clean Architecture nach Robert C. Martin. © Hendrik Lösch

Folgt man diesem Ansatz konsequent, erreicht man am Schluss das Architekturmodell der Clean Architecture [4] (s. Abb.4). Dieses hat im Gegensatz zur beliebten Dreischichtenarchitektur Kreisförmige Schichten, welche sich zunächst um die Datenobjekte, dann die Geschäftsregeln, die spezifischen Applikationsregeln bis hin zu den Schnittstellen zu äußeren Systemen legen. Gibt es beispielsweise eine Anfrage über einen Webservice, welcher die äußerste Schicht darstellt, wird dieser die Daten über Interface-Adapter soweit konvertieren, dass sie z. B. von der Anwendungslogik auf Berechtigungen geprüft und anschließend von der Geschäftslogik verarbeitet werden können. Diese Logik liefert das Ergebnis zurück, welches anschließend für Datenbank aufbereitet werden und darin gespeichert wird. Wichtig ist hierbei, dass jede Schicht immer nur mit der spricht die näher am Kern liegt. So darf die Applikationslogik zwar die Geschäftslogik akquirieren, nicht aber mit der Datenbank kommunizieren. Das ist wiederum nur den entsprechenden Konvertern der äußeren Schicht erlaubt. Beachtet man dies, erhält man ein weithin gut verständliches und leicht zu testendes System.

nach Oben
Autor

Hendrik Lösch

Hendrik Lösch legt den Schwerpunkt seiner Arbeit auf Entwicklung und Pflege von Software im industriellen und medizinischen Umfeld.
>> Weiterlesen
botMessage_toctoc_comments_929