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
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:
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
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.
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.
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!