Seit Jahrzehnten wird auf die Verwendung von goto-Statements nahezu vollständig verzichtet. Liest man die Veröffentlichungen von Edsger W. Dijkstra, haben diese trotz ihres Alters nichts an ihrer Aussagekraft verloren: Software muss so strukturiert sein, dass sie über viele Jahre wandelbar bleibt. Ferner müssen wir Strukturen schaffen, deren Korrektheit wir „beweisen“ können. Möglicherweise war die Informatik in ihren Anfängen etwas zu optimistisch in Hinblick auf die Beweisbarkeit der Korrektheit. Doch seit der Einführung der ersten Test-Frameworks ist im Bereich der Korrektheit vieles vorangekommen. Und wir erkennen: Das goto-Statement ist nahezu vollständig verschwunden und durch Methodenaufrufe ersetzt worden.
Abhängigkeiten
Ist eine vergleichbare Entwicklung beim Umgang mit Abhängigkeiten denkbar? Bei Abhängigkeiten setzen wir heute ganz überwiegend auf das Dependency Inversion Principle (DIP). In Abbildung 1 ist eine direkte Abhängigkeit von A nach B zu sehen.
Abb. 1: Direkte Abhängigkeit
Beispielhaft zeigt Listing 1 die Klasse NewCustomer, in der eine direkte Abhängigkeit zur Klasse CustomerRepository besteht. Bei der Betrachtung einer solchen direkten Abhängigkeit können unterschiedliche Aspekte bewertet werden. Ein mentales Modell der Struktur aufzubauen, fällt uns leichter, wenn Details von Abstraktem abhängen. Bin ich als Entwickler mit Details konfrontiert, komme ich schnell an eine Grenze, an der ich keine weiteren Details mehr berücksichtigen kann. Ich muss mein mentales Modell auf eine Basis von Abstraktionen abstützen. Insofern brauchen wir eine Lösung für das Problem, dass manchmal die Richtung der Abhängigkeiten falsch herum verläuft: Sobald Abstraktes von Details abhängt, sollten wir durch Anwendung des Dependency Inversion Principle (DIP) die Richtung umkehren. Abbildung 2 zeigt, wie die geänderte Struktur aussieht. Nun ist das Abstrakte A nicht mehr vom Detail B abhängig, sondern von einem Interface oder Kontrakt (engl. Contract). Ein Interface ist ebenfalls abstrakt, wodurch nun Abstraktes von Abstraktem abhängt. Auch für B ändert sich die Situation, da das konkrete B nun den abstrakten Kontrakt realisieren muss. Somit ist auch hier Detailreiches von Abstraktem abhängig. Die beispielhafte Klasse NewCustomer kann durch Einführen des Interface ICustomerRepository auf Dependency Inversion umgestellt werden, wie Listing 2 zeigt. Hier kommt nun bereits Dependency Injection (DI) zum Einsatz, um die Klasse NewCustomer vollständig unabhängig zu machen von der konkreten Implementierung des Interface.
Listing 1: In der Klasse NewCustomer besteht eine direkte Abhängigkeit zur Klasse CustomerRepository
Listing 2: Die Klasse NewCustomer kann durch Einführen des Interface ICustomerRepository auf Dependency Inversion umgestellt werden
Abb. 2: Dependency Inversion Principle (DIP)
Testbarkeit
Das zweite Problem, welches durch das DIP gelöst wird, ist die Testbarkeit. Die ursprüngliche Struktur in Abbildung 1 beziehungsweise Listing 1 ist nur mittels Integrationstest testbar. Durch die direkte Abhängigkeit kann im Test nur A aufgerufen werden, was implizit zur Verwendung von B führt. Mit entsprechenden Tools kann dieses Problem gelöst werden. So lassen es manche Mock-Frameworks zu, auch Konstruktoren im Test durch Attrappen zu ersetzen. Damit lassen sich dann Unittests für A schreiben, die dessen Funktionsweise isoliert prüfen, ohne dass B beteiligt ist. Die Einführung eines Interface ist häufig eine elegantere Lösung für diese Herausforderung. Dann kann nämlich im Test eine Attrappe für den Kontrakt BC an A übergeben werden. Es sind dann keine spezialisierten Tools erforderlich und die Tests sind häufig leichter verständlich. Interfaces und die anschließende Einführung von Dependency Injection (DI) sind ein weitverbreitetes Mittel, mit dem Codestrukturen isoliert unter Test gestellt werden. Für den Beispielcode würde dies bedeuten, im Test eine Attrappe für das Interface ICustomerRepository in die Klasse NewCustomer zu injizieren. So ist der Test in der Lage, die Logik zu überprüfen, die in der Methode HandleUseCase enthalten ist, ohne dass dabei eine konkrete Datenbank zum Einsatz kommt.
Integration Operation Segregation Principle
Bei Anwendung des DIP bleibt allerdings ein ungelöstes Problem zurück: In A sind weiterhin Aspekte vermischt. Das Single Responsibility Principle (SRP) besagt, dass es in jeder Funktionseinheit nur einen Grund für Veränderung geben soll. Nach Einführung des Interface gibt es in A nach wie vor zwei Gründe für Veränderung. Zum einen kann sich die Logik von A ändern, zum anderen kann sich die Verwendung, der Aufruf, von BC ändern. In A sind zwei Aspekte vermischt, die wir mit Integration und Operation bezeichnen. Als Spezialform des Single Responsibility Principle (SRP) ergibt sich auf diese Weise das Integration Operation Segregation Principle (IOSP), siehe Abbildung 3. Eine Methode kann dafür verantwortlich sein, andere der zur Lösung gehörenden Methoden aufzurufen. Dann ist ihre Zuständigkeit die Integration. Eine Methode kann aber auch Logik enthalten und wir bezeichnen sie dann als Operation. Das IOSP besagt, dass die beiden Verantwortlichkeiten nicht vermischt werden sollen. Betrachten wir das Dependency Inversion Principle (DIP) nochmals etwas näher. Dieses Prinzip möchte zwei Probleme lösen:
- Details sollen von Abstraktem abhängig sein und nicht umgekehrt.
- Durch die Einführung des Kontrakts soll die Testbarkeit vereinfacht werden.
Abb. 3: Integration Operation Segregation Principle (IOSP)
Man kann zusätzlich anführen, dass es nun verschiedene Implementierungen des Kontrakts geben kann und somit die Austauschbarkeit hergestellt wird. Dies ist allerdings in der Praxis eher selten anzutreffen und wird hier nicht weiter betrachtet. Alternative Implementierungen eines Interface verwenden zu können, bleibt weiterhin die Aufgabe des DIP. Insofern soll das IOSP die beiden oben dargestellten Probleme ebenfalls lösen, andernfalls wäre es wenig sinnvoll vorzuschlagen, das DIP in großen Teilen durch das IOSP abzulösen. Folglich muss also die Struktur, die durch das IOSP entsteht, ebenfalls in Richtung der Abstraktion verlaufen und leicht testbar sein. Der Integrationscode in I ist abstrakter Code. Das abstrakte I hängt also vom abstrakten A sowie vom detailreichen B ab. Für A ändert sich damit etwas in Bezug auf das SRP: Der Integrationsanteil, der vormals in A enthalten war, um B mittels BC aufzurufen, ist nun nach I gewandert. Es bleibt der Logikanteil übrig. Daher können wir A nun als Operation bezeichnen. Für B galt dies bereits vorher, da B ohnehin keine andere Methode der Lösung aufruft. Somit sind die Verantwortlichkeiten nun klar getrennt: I integriert die beiden Operationen A und B. Bezogen auf das Beispiel führt dies zu der in Listing 3 gezeigten Änderung am Code. Die HandleUseCase-Methode ist nun eine Integrationsmethode. Sie ruft lediglich andere Methoden der Anwendung auf. Die aufgerufenen Methoden PrepareCustomerForInsert und Insert sind dagegen Operationen.
Listing 3: Die Verantwortlichkeiten sind hier klar getrennt
Wandelbarkeit
Betrachten wir die Struktur des IOSP in einem weiteren Aspekt: Da A zuvor als abstrakt gegolten hat, muss nun die Frage beantwortet werden, ob das geänderte A nach wie vor abstrakt ist. Es entfällt der Aufruf von B. Man kann sagen, dass die Abstraktheit von A dadurch etwas konkreter geworden ist. A ist nun ausschließlich zuständig für die eigene Logik und somit etwas konkreter.
B galt bereits zuvor als konkret beziehungsweise detailliert, da dies der Ausgangspunkt für die Überlegung zum DIP darstellte (abstraktes A ist abhängig von konkretem B). Nach der Umstellung gemäß IOSP ergibt sich somit das Bild, dass ein abstraktes I abhängig ist von einem etwas konkreter gewordenen A und einem ohnehin konkreten B. Widerspricht das nicht dem DIP? Betrachten wir dazu die drei unterschiedlichen Strukturen aus Sicht der Wandelbarkeit. Hier stellt sich beispielsweise die Frage, was mit anderen Methoden passiert, wenn sich B ändert. Dabei können wir zwei Änderungen unterscheiden:
- Die Signatur von B wird geändert.
- Das Verhalten von B wird geändert.
Variante 1: A ist abhängig von B (direkte Abhängigkeit)
Wenn B geändert wird, muss A potenziell ebenfalls geändert werden, da sich Änderungen in der Gegenrichtung der Abhängigkeit auswirken. Auf eine Verhaltensänderung von B in A zu reagieren, fällt potenziell schwer, da A eigene Logik enthält, die dann angepasst werden muss. Ändert sich die Signatur von B, ist A davon unmittelbar betroffen. Auf diese in A zu reagieren, ist technisch eher einfach, weil mögliche Konflikte von der Entwicklungsumgebung, dem Compiler oder Interpreter erkannt werden. Allerdings enthält A detailreichen Code, sodass der Blick auf den Aufruf von B eventuell etwas verstellt ist.
Schwierig ist an diesem Konstrukt der direkten Abhängigkeit vor allem, dass die Logik in A potenziell an das geänderte Verhalten von B angepasst werden muss. Die Signaturänderung kann zwar nachgezogen werden, ist aber aufgrund der Details, die in A enthalten sind, nicht trivial.
Variante 2: A und B sind abhängig von BC (DIP)
In diesem Fall kann sich die Änderung an der Signatur von B nicht unmittelbar auf A auswirken. Allerdings ist hierbei zu unterscheiden, ob der Kontrakt BC bei der Signaturänderung in B weiterhin eingehalten werden kann. Wenn das der Fall ist, ergibt sich der Vorteil, dass die Signatur von B „hinter den Kulissen“ von BC geändert werden kann und dies keinen Einfluss auf A hat. Dies ist ein Vorteil, den Variante 1 nicht bieten kann. Allerdings ist dies in der Realität eher selten anzutreffen. Eine Signaturänderung in B lässt sich nicht verbergen, wenn der Kontrakt BC als Interface realisiert ist. Dies wäre nur möglich, wenn BC als abstrakte Basisklasse realisiert ist, in der durch ein Mapping auf die geänderte Signatur von B reagiert werden kann. Allerdings: Dann wäre dieselbe Strategie auch in B anwendbar. Logikänderungen in B wirken sich auch bei dieser Variante auf A aus. Verhält sich B anders, muss darauf potenziell in A reagiert werden.
Variante 3: I ist abhängig von A und B (IOSP)
Eine Signaturänderung von B wirkt sich hier auf I aus. Die Anpassungen in I sind technisch die gleichen wie bei Variante 1. Allerdings ist I ausschließlich von den Signaturänderungen betroffen und enthält nur abstrakten Code. Daher fällt die Änderung eher leichter als bei Variante 1. Logische Änderungen von B wirken sich nicht auf I aus, sondern weiterhin lediglich auf A.
Wir können hier also festhalten, dass bei allen drei Varianten Änderungen an A notwendig werden können, wenn sich das Verhalten von B ändert. Dies ist nicht zu vermeiden, so lange es eine logische Abhängigkeit zwischen den beiden Methoden gibt. Das Single Responsibility Principle (SRP) sowie Separation of Concerns (SoC) können dieses Problem mildern. Somit geht es bei der Bewertung der drei Strukturen in der Hauptsache um die Frage, wie schwer es fällt, auf Signaturänderungen zu reagieren. Bei Variante 2 (DIP) ergibt sich der Vorteil, dass hier potenziell eine Änderung hinter dem Kontrakt BC versteckt werden kann. Dies ist allerdings auch ohne den Kontrakt BC möglich, in dem die Änderung hinter der öffentlichen Schnittstelle von B versteckt wird. Insofern ist dies zu vernachlässigen. Die Varianten 1 (direkte Abhängigkeit) und 3 (IOSP) unterscheiden sich nur leicht. In beiden Fällen muss an I beziehungsweise A der Aufruf von B angepasst werden. Das fällt in I etwas leichter, da I ausschließlich Methodenaufrufe und keine eigene Logik enthält.
Bei Änderung der Logik von B hat keine der drei Varianten einen deutlichen Vorteil. Die Verhaltensänderung kann sich auf A auswirken, sodass A dann angepasst werden muss. Das DIP bietet demnach nur dann einen echten Vorteil, wenn damit unterschiedliche Umsetzungen des Kontrakts realisiert werden. Insbesondere wird dabei die Umsetzung in Form von Attrappen am häufigsten verwendet, um eine bessere Testbarkeit von A zu erreichen. Doch genau dieser Aspekt wird durch das IOSP noch einfacher erreicht. A ist bereits freigestellt, sodass hier keine Dependency Injection und Attrappen für eine leichte Testbarkeit erforderlich sind. Im Hinblick auf die Testbarkeit liegt das IOSP hier klar im Vorteil gegenüber dem DIP. Tests mit Attrappen sind immer aufwendiger, als wenn diese nicht benötigt werden. Ein weiterer Unterschied ergibt sich aus Sicht der Wandelbarkeit. Hier muss betrachtet werden, wie leicht es fällt, den Code in den Methoden A, B und I zu verstehen. Bei A gibt es hier keinen Unterschied zwischen der direkten Abhängigkeit und dem DIP. In beiden Fällen sind in A Aufrufe von B beziehungsweise BC enthalten. Die Logik in A enthält somit auch Aufrufe anderer Logik und ist dadurch schwerer verständlich, als die IOSP-Variante. Bei der IOSP-Variante ist A reduziert auf Logik, so wie es für B in allen drei Varianten gilt. Auch I ist leicht zu lesen, da es sich hier um Integrationscode auf hohem Abstraktionsniveau handelt. I enthält keine Aufrufe von Framework oder Runtime-Methoden und auch keine Ausdrücke.
Fazit
Das Dependency Inversion Principle (DIP) hat gegenüber dem Integration Operation Segregation Principle (IOSP) einen leichten Vorteil bei Signaturänderungen, wenn diese hinter dem Kontrakt verborgen werden können. In der Praxis ist dies nicht zu beobachten. In aller Regel kommen Interfaces als Kontrakt zum Einsatz, die keine Möglichkeit bieten, Kontraktänderungen zu verbergen. Setzt man abstrakte Klassen ein, kann die Übersetzung einer Signaturänderung zwar im Kontrakt stattfinden, kann dann aber auch direkt in B vorgenommen werden. Für die Testbarkeit ist das DIP die aufwendigere Lösung gegenüber dem IOSP. Auch bei der Wandelbarkeit ist das IOSP deutlich im Vorteil. Insofern bleibt das DIP lediglich für Fälle übrig, in denen tatsächlich mit unterschiedlichen konkreten Implementierungen des Kontrakts gearbeitet werden soll. Aus meiner langjährigen Erfahrung als Trainer und Berater kann ich berichten, dass das IOSP für die Teilnehmer das mit Abstand wichtigste Prinzip wurde. Die positiven Auswirkungen auf die Testbarkeit und das leichte Verständnis der Codestruktur sind immens. In diesem Sinne ist es an der Zeit, das DIP um das IOSP zu ergänzen. Das reflexartige Einziehen von Interfaces sollte überdacht werden. ||
Weitere Informationen
Communication of the ACM, Volume 11, Number 3, March 1968, siehe: https://homepages.cwi.nl/~storm/teaching/reader/Dijkstra68.pdf
[Dij70] https://www.cs.utexas.edu/users/EWD/ewd02xx/EWD249.PDF