Bild von Stefan Lieser
Stefan Lieser

Abhängigkeiten reduzieren

 

Bei diesem Beitrag handelt es sich um eine überarbeitete Version. Er wurde zuvor bereits bei refactoring-legacy-code.net veröffentlicht.

Abhängigkeiten machen Ärger 

 

Abhängigkeiten sind das Grundübel der Softwareentwicklung. Viele Entwickler nehmen sie als gegeben hin. Schließlich gibt es doch das Dependency Inversion Principle (DIP). Und es gibt zahlreiche Dependency Injection Frameworks wie Unity, Spring, Castle Windsor, StructureMap und wie sie alle heißen. Diese Frameworks widmen sich dem Thema Abhängigkeiten und reduzieren den Aufwand, den man als Entwickler beim automatisierten Testen damit hat. Meiner Beobachtung nach führt der unbedachte Umgang mit Abhängigkeiten jedoch zu mehreren Problemen, die auch das DIP nicht vollständig löst. Darum soll es im Folgenden gehen und natürlich darum, wie Abhängigkeiten reduziert werden können. Betrachten wir zunächst einige Gründe, weshalb Abhängigkeiten die Softwareentwicklung erschweren.

Abhängigkeiten erschweren das Verständnis der Software

 

Wenn eine Funktionseinheit von einer anderen abhängig ist, wird es schwieriger, sie zu verstehen. Funktionseinheiten, die keine Abhängigkeiten haben, machen ihr Ding, fertig. Sie sind leichter verständlich, weil die Funktionalität vollständig sichtbar ist. Es wird eben keine Teilfunktionalität an eine andere Funktionseinheit übertragen, sondern die Aufgabe vollständig alleine gelöst. Natürlich fällt das Verständnis nur dann leicht, wenn die Funktionseinheit eine klare Zuständigkeit hat (Single Responsibility Principle (SRP)) und überschaubar groß ist. Das gilt allerdings in gleichem Maße für Funktionseinheiten mit Abhängigkeiten.

Abhängigkeiten erschweren das automatisierte Testen

 

Sobald Abhängigkeiten im Spiel sind, werden die Tests aufwendiger. Natürlich kann ich immer Integrationstests schreiben, die eine Klasse inklusive ihrer Abhängigkeiten testen. Am Ende sollte ich sogar unbedingt einige Integrationstests schreiben, um so sicherzustellen, dass die beteiligten Einzelteile korrekt zusammenwirken. Doch sollen Integrationstests immer in der Unterzahl sein, gegenüber Unit Tests. Und eine Klasse mit Abhängigkeiten isoliert zu testen bedeutet, Interfaces und Attrappen müssen her. Und siehe da, schon wieder kommen Frameworks zur Hilfe: diesmal die Mock Frameworks wie Rhino Mocks, JustMock, Moq, NSubstitute, FakeItEasy, etc. Doch auch hier gilt: statt Symptome zu lindern ist es besser, die Krankheit zu heilen.

Abhängigkeiten erschweren Änderungen

 

Soll eine Funktionseinheit geändert werden, von der andere abhängig sind, muss darauf geachtet werden, dass die Abhängigen ggf. angepasst werden. Bei einer 1:1 Beziehung ist das noch zu verkraften. Sind jedoch mehrere Abhängigkeiten im Spiel, kann die Änderung schnell aufwendig werden. Ruft bspw. eine Methode aus dem Bereich der Domänenlogik immer wieder Methoden aus dem Bereich der Persistenz auf, ist eine Änderung eher schwierig.

Ein Beispiel

 
Im Folgenden betrachten wir eine stark vereinfachte Anwendung, die aus einem einfachen Datenfluss besteht. Vom Userinterface (Ui) bzw. dem Human Machine Interface (HMI) fließen die Benutzereingaben zur Domänenlogik. Dort erfolgt eine Transformation: aus dem x wird ein y. Zuletzt werden die transformierten Daten in einer Datenbank persistiert. Die folgende Abbildung zeigt dies in einem Datenflussdiagramm. Jeder Pfeil steht für einen Datenfluss. Im Beispiel fließt die Eingabe x von der Ui zur Domain. Anschließend fließt das y von der Domain zur Datenbank.
einfacher Datenfluss - Clead Code Academy - Stefan Lieser

Dass die drei Aspekte Benutzerschnittstelle (Ui), Domänenlogik (Domain) und Datenbankzugriff (DB) zu trennen sind, ist klar. Dies ergibt sich aus dem Single Responsibility Principle (SRP). Doch wie bringen wir die beteiligten Klassen dazu, in geeigneter Weise zusammen zu arbeiten? Meistens wird folgende Abhängigkeitsstruktur gewählt:

einfacher Datenfluss - Abhängigkeiten falsch- Clead Code Academy - Stefan Lieser

Die Benutzerschnittstelle ist abhängig von der Domänenlogik. Sie ruft diese auf. Natürlich wird für die Domänenlogik ein Interface dazwischen gestellt. Die Ui ist also vom Interface abhängig und nicht direkt von der Domänenlogik. Genauso verhält es sich mit der Domänenlogik und dem Datenbankzugriff. Die Domänenlogik ist abhängig vom Datenbankzugriff. Auch hier: nicht unmittelbar, sondern über ein Interface. Diese Vorgehensweise ergibt sich aus dem Dependency Inversion Principle (DIP). Doch werden dadurch schon alle Probleme optimal gelöst?

Ich meine nein, denn Tests der Domänenlogik sind nun relativ aufwendig, da Attrappen für die Datenbankzugriffe verwendet werden müssen. Man kann die Domänenlogik nicht einfach so aufrufen, da sie ja mit der Datenbank kommuniziert. Also wird beim Testen eine Attrappe reingereicht, dann kommuniziert die Domänenlogik mit der Attrappe statt der realen Datenbank. Natürlich wird dadurch schon einiges besser, denn nun kann ich die Domänenlogik überhaupt erst isoliert automatisiert testen, ohne jedesmal eine Datenbank aufsetzen zu müssen.

Abhängigkeiten reduzieren

 
Doch es geht noch besser. Die gleiche Funktionalität kann nämlich erreich werden, in dem über die drei beteiligten Klassen zwei weitere gestellt werden, die ausschließlich für die Abhängigkeiten zuständig sind. Nun ist der Controller dafür zuständig, die Ui mit dem Rest der Anwendung zu integrieren. Dazu bindet sich der Controller an Events der Ui. Gibt der Benutzer Daten ein und drückt anschließend auf eine Schaltfläche, landen die eingegebenen Daten per Event beim Controller. Die Ui hat nun keine Abhängigkeiten zur Domänenlogik mehr und auch nicht zum Controller, sondern löst lediglich Events aus, an die sich der Controller registriert hat.
 

Der Controller liefert die Daten anschließend an den Interactor. Dieser integriert die Domänenlogik mit den Ressourcenzugriffen. Damit hat nun auch die Domäne keine Abhängigkeit mehr. Stattdessen macht die Domänenlogik ihr Ding und liefert ggf. Daten zurück zum Interactor. Der Interactor liefert diese Daten dann an die Datenbank, damit sie dort persistiert werden. Es ergibt sich somit zur Laufzeit der gleiche Datenfluss wie eingangs: Daten fließen von der Ui über die Domänenlogik zur Datenbank. Natürlich fließen die Daten nicht auf die exakt gleiche Weise, sondern nehmen den “Umweg” über den Controller sowie den Interactor.

IOSP Integration Operation Segregation Principle

Testbarkeit

Der größte Vorteil entsteht hier für die Testbarkeit der Domänenlogik: die ist nämlich jetzt nicht mehr abhängig von den Datenbankzugriffen. Im ersten Beispiel war die Domänenlogik noch eine void-Methode mit einem Seiteneffekt. Nun ist sie eine Funktion.

Im Test wird die Domänenlogik mit Eingabedaten aufgerufen, aus denen sie Ausgabedaten produziert. Solcher Code ist leicht automatisiert zu testen. Attrappen werden keine benötigt. Auch das Interface wird nun nicht mehr benötigt und auf Dependency Injection kann verzichtet werden.

Lesbarkeit

Da die Domänenlogik keine Abhängigkeit mehr zur Datenbank hat, ist sie leichter verständlich. Sie erhält Eingabedaten und produziert daraus Ausgabedaten. Die Zuständigkeit, die Datenbank in geeigneter Weise anzusprechen, ist herausgezogen worden in den Interactor. Um die Domänenlogik zu verstehen, muss nun nicht mehr verstanden werden, wie sich die Datenbanklogik verhält, da die Domänenlogik zu dieser keine Abhängigkeit mehr hat.

Abhangigkeiten Web Softwareentwicklung Abhängigkeiten reduzieren

Webanwendungen

Im Webumfeld bietet sich eine Variante des oben dargestellten Modells an. Bei Webanwendungen ist es üblich, dass die obersten Klassen von der Infrastruktur instanziiert werden. URLs werden auf Klassen geroutet. Diese werden dann von der Infrastruktur, sei es ASP.NET, Spring, o.ä. instanziiert. In einem solchen Modell ist es sehr hilfreich, wenn die Abhängigkeiten über Dependency Injection aufgelöst werden können. Schließlich sorgt dann die Infrastruktur für das Instanziieren aller benötigten Objekte. Für solche Fälle bietet sich die folgende Struktur von Abhängigkeiten an.

Hier wird die Ui vom Framework instanziiert. Der Interactor kann von der Infrastruktur in die Ui injiziert werden. Dabei werden die benötigten Abhängigkeiten zur Domäne und der Datenbank ebenfalls injiziert, so wie es im Ausgangsbeispiel schon der Fall war. Da der Interactor allerdings lediglich eine dünne Integrationsschicht darstellt, muss diese Klasse nicht isoliert getestet werden. 

Daher kann auch bei diesem Modell auf den Einsatz von Attrappen im Test verzichtet werden. Beachten Sie, dass nach wie vor Integrationstests für den Interactor erforderlich sind. Domänenlogik und Datenbank sind wieder Blätter im Abhängigkeitsbaum. Somit sind diese Teile leicht isoliert zu testen, ohne dass hier Attrappen zum Einsatz kommen müssen.

Fazit

Sobald man beginnt, mit Abhängigkeiten bewusst umzugehen, sie zu planen, verlieren sie ihren Schrecken. Letztlich läuft es darauf hinaus, konsequent das IOSP anzuwenden. So können wir Abhängigkeiten reduzieren. IOSP steht für Integration Operation Segregation Principle. Es besagt, dass Integration und Operation zu trennen sind. Die Aufgabe der Integration ist es, mit Abhängigkeiten umzugehen. Hier sammeln sich die Abhängigkeiten an wenigen Punkten. 

Andererseits enthält die Integration aber keine Domänenlogik. Das ist die Zuständigkeit der Operationen. Diese sind ausschließlich für Logik zuständig und im Umkehrschluss nicht mit Abhängigkeiten belastet. Im Kleinen liefert das IOSP Methoden, die leicht verständlich und mit klarer Zuständigkeit versehen sind. Ferner wird dann implizit das Single Level of Abstraction (SLA) Prinzip eingehalten. Im Großen führt das IOSP zu Strukturen, bei denen auf der Ebene von Klassen, Bibliotheken, Paketen und Komponenten die Abhängigkeiten isoliert und freigestellt sind.

In den meisten Anwendungen spielt die Musik in der Domänenlogik. Diese sollte daher leicht verständlich und gut testbar sein. Sobald die Domänenlogik von Abhängigkeiten befreit ist, gelingt dies deutlich leichter.

Das dazu passende Training

Die hier beschriebene Vorgehensweise wird in unserem Training Clean Code Developer Basics behandelt. Sie lernen dort, welche Herausforderungen das Dependency Inversion Principle (DIP) offen lässt und wie diese mit dem Integration Operation Segregation Principle (IOSP) gelöst werden. Nehmen Sie gerne Kontakt mit uns auf!

Unsere Seminare

course
Clean Code Developer Basics

Prinzipien und Tests – Das Seminar wendet sich an Softwareentwickler, die gerade beginnen, sich mit dem Thema Softwarequalität auseinanderzusetzen. Es werden die wichtigsten Prinzipien und Praktiken der Clean Code Developer Initiative vermittelt.

zum Seminar »
course
Clean Code Developer Advanced

Mit Flow Design von den Anforderungen zum Clean Code – Lernen Sie mit Flow Design einen Softwareentwicklungsprozess kennen, der Sie flüssig von den Anforderungen zum Clean Code führt.

zum Seminar »
course
Clean Code Developer Trainer

Seminare als Trainer durchführen – Dieses Seminar wendet sich an Softwareentwickler, die ihr Wissen über die Clean Code Developer Prinzipien und Praktiken bzw. über Flow Design als Trainer an andere weitergeben möchten.

zum Seminar »
course
Clean Code Developer CoWorking

Online CoWorking inkl. Coaching –
Wir werden häufig gefragt, was man als Entwickler tun könne, um kontinuierlich dran zu bleiben am Thema Clean Code Developer. Unsere Antwort: Treffen Sie sich regelmäßig wöchentlich online mit anderen Clean Code Developern.

zum Seminar »

Kommentar verfassen

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert

de_DEGerman