Abhängigkeiten reduzieren

Abhängigkeiten machen Ärger

Abhängigkeiten sind das Grundübel der Softwareentwicklung. Viele Entwickler nehmen sie als gegeben hin. Schließlich gibt es doch zahlreiche Dependency Injection Frameworks wie Unity, Spring.NET, 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 damit hat. Meiner Beobachtung nach führt der unbedachte Umgang mit Abhängigkeiten jedoch zu mehreren Problemen. Darum soll es im Folgenden gehen und natürlich darum, wie Abhängigkeiten reduziert werden können.

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 und überschaubar groß ist. Das gilt allerdings in gleichem Maße für Funktionseinheit 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 testet. 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ängige vorhanden, kann die Änderung schnell aufwendig werden.

Ein Beispiel

Im folgenden betrachten wir eine stark vereinfachte Anwendung, die aus einem einfachen Datenfluss besteht. Von der Ui fließen die Benutzereingaben zur Domänenlogik. Dort erfolgt eine Transformation. Zuletzt werden die transformierten Daten in einer Datenbank persistiert.

Dass die drei Aspekte Benutzerschnittstelle (Ui), Domänenlogik und Datenbankzugrif zu trennen sind, ist klar. 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. 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 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 für die Abhängigkeiten zuständig sind.

Nun ist der Controller dafür zuständig, die UZi 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 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.

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 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 die Interfaces werden nun nicht mehr benötigt.

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:

Nun 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, die auf der Ebene von Klassen, Bibliotheken und Komponenten die Abhängigkeiten isoliert und freistellt.

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.

Beratung / Coaching

Gerne komme ich zu Ihnen und unterstütze Sie dabei, die Abhängigkeiten in Ihrer Codebasis in den Griff zu kriegen. Gemeinsam erarbeiten wir einen Ist- und einen Sollzustand und ich zeige Ihnen Wege auf, wie die Situation mittels Refactoring verbessert werden kann. Meine entsprechenden Angebot finden Sie hier.

Schreibe einen Kommentar