Inversion of Control

Vergleichbar mit der Konstruktion eines Wolkenkratzers, müssen bei der Erstellung einer Software Architektur-Richtlinien definiert und berücksichtigt werden. Wird dies nicht befolgt, stürzt die Konstruktion über kurz oder lang ein.
Inversion of Control gibt dem Entwickler ein effektives Tool an die Hand, um Software auf Klassenebene so solide zu bauen, dass auch bei größeren Umbaumaßnahmen oder langjähriger Weiterentwicklung die Architekturaspekte Erweiterbarkeit, Wiederverwendbarkeit, Testbarkeit nicht in Mitleidenschaft geraten.

Große Softwaresysteme bestehen üblicherweise aus mehreren Modulen mit unterschiedlichen Aufgaben. Typischerweise sind solche Module ineinander verschachtelt und hängen voneinander ab. Auch die Klassen in einem Modul sind von anderen Klassen unterschiedlich stark abhängig. Aus der dadurch entstehenden starken Verwobenheit von Klassen resultiert oft die Notwendigkeit beiÄnderungen in einer Klasse, deren abhängige Klassen auch anpassen zu müssen. Das macht ein großes System schwierig erweiter- und wartbar - und verursacht letztendlich höhere Kosten.

Um dieses Problem zu vermeiden und eine höhere Software-Qualität zu erreichen, werden Klassen und Module so gebaut, dass eine möglichst lose Kopplung zwischen den unterschiedlichen Komponenten vorherrscht. Dadurch wird vermieden, Klassen anpassen zu müssen, die nicht angefasst werden wollten.

Wie kann man also eine losere Kopplung erreichen? Genau hier kommt das Prinzip „Inversion of Control“ zu Hilfe.
 

Beispiel-Szenario

Um das Problem genauer zu betrachten, wird im Folgenden ein einfaches Beispiel-Szenario vorgestellt, für das zwei Implementierungsansätze in Java vorgestellt werden.

Anforderungen

  • Dokumente sollen durchsuchbar sein
  • Die FAST-Suchmaschine soll verwendet werden
  • evtl. soll dieses Jahr auf eine andere Suchmaschine geschwenkt werden
  • Zur Qualitätssicherung sollen Unit-Tests geschrieben werden

Man könnte die Umsetzung der Anforderungen in zwei Module aufgeteilt umsetzen. Ein Modul, das die Implementierungen der verschiedenen Suchanbieter zur Verfügung stellt (SearchProvider) und ein zweites Modul (SearchService), das den Suchservice implementiert und das einen der implementierten Suchanbietern benutzt und damit von dem SearchProvider abhängig ist.

Implementierungsansatz 1 (enge Kopplung - so besser nicht)

Beim ersten Ansatz werden die Anforderungen mit enger Kopplung zwischen den beiden Modulen umgesetzt. Die folgenden Code-Ausschnitte skizzieren dieImplementierungen in den beiden Modulen:

SearchProvider

SearchService

Der SuchService hängtdirekt von dem FAST-SearchProvider ab,  die Erstellung des SearchProviders wird also direkt im SearchService durchgeführt. Genau dadurch entsteht eine enge Kopplung zwischen diesen Modulen.

Die direkte Abhängigkeit führt dazu, dass bei einem späteren Austausch des SearchProviders auch des SearchService-Modul angepasst werden muss. An diesem Beispiel sieht die Anpassung in den beiden Modulen ziemlich einfach aus, aber bei einem komplexeren System könnte dies zu zusätzlichen Aufwänden führen wie z.B. Deployment von mehreren Komponenten oder Durchführung von mehreren Tests, was weitere Risiken und Aufwände mitbringen könnte.

Unit-Tests zum Suchservice:

11.png

Ein anderes Problem taucht auf, wenn man die implementierte Funktionalität testen möchte. Bei einem Unittest ist das Ziel nur eine Unit automatisiert zu testen. An dem Code-Beispiel ist die Unit der SearchService. Um den SearcService als Unit zu testen, sollte der SearchProvider durch einen MockSearchProvider ersetzt werden. Dies ist über Anpassungen der SearchProviders möglich. Wird das nicht gemacht, schlägt der Unit-Test auf die Suchmaschine durch und wird zu einem Integrationstest bei dem der Suchserver erreichbar sein muss.

Was ist aber die wirkliche Ursache für die angedeuteten Probleme?

Die Ursache ist eigentlich die direkte Abhängigkeit des SearchService von dem FAST-SearchProvider.

Wer den Code genauer anschaut, erkennt, dass diese direkte Abhängigkeit nicht zwingend notwendig ist. Die Variable, die auf die SearchProvider-Implementierung referenziert, wird eigentlich als eine Instanz von dem SearchProvider-Interface deklariert, d.h. als eine Abstraktion von dem SearchProvider. In dem restlichen Codeabschnitt, wo diese Variable verwendet wird, werden Funktionalitäten genau aus der Definition der Abstraktion aufgerufen. Das heißt, dass der FAST-SearchProvider dort überhaupt nicht bekannt ist oder sein sollte und eigentlich nur bei der Initialisierung der Variable referenziert wird.

Wenn die Initialisierung der Variable außerhalb des SearchServices stattfinden könnte, würde er nicht mehr von der konkreten Implementierung - dem FAST-SearchProvider -  abhängen, sondern nur von der definierten Abstraktion.

Eine solche Lösung würde den Austausch von dem SearchProvider vereinfachen, da in diesem Fall der SearchService nicht angepasst werden muss. Die Einbindung des MockServices im Unittest wäre dann einfach.
 

Implementierung 2 (losere Kopplung - so ist besser)

Der folgende Implementierungsansatz zeigt, wie man die Anbindung von dem SearchService zu einem konkreten SearchProvider außerhalb von dem SearchService auslagern könnte.  Dabei kommt das Inversion of Control Framework Google Guice zur Hilfe.

Mit Guice sieht die Implementierung des SearchServices so aus:

Die Annotation @Inject gibt dem Framework den Hinweis, dass hier ein SearchProvider angebunden (injected) werden sollte. Die Anbindungsdefinition geschieht dann außerhalb von dem SearchService in einer separaten Klasse, z.B. für den MockSearchProvider wie folgt:

Man kann im Code die Anbindungskonfiguration verwenden, um den entsprechenden Service zu benutzen, z.B. um in den Unittests für den SearchService den MockSearchProvider statt den echten SearchProvider anzuwenden:

Bei diesem Ansatz wurde die Kontrolle der Erzeugung von dem konkreten SearchProvider von dem SearchService an das Framework übergeben („Inversion of Control“). Damit könnte man dann Anbindungen umdefinieren oder austauschen und dies, ohne den SearchService anpassen zu müssen. Dadurch wird die lose Kopplung zwischen dem SearchService-Modul und dem SearchProvider-Modul erreicht.
 

Fazit - was bringt Inversion of Control?

Trennung zwischen Nutzung und Erzeugung von Objekten

- > losere Kopplung

  • Einfache Austauschbarkeit
  • Fokus auf das Design
  • Sicht nur auf gebrauchte Funktionalität
  • Schneller und leichter Austausch von Komponenten
  • Weniger Nebeneffekte

-> bessere Testbarkeit

  • Verwendung von Mocks
  • Schwerpunkt auf das Testen von dem Klassenverhalten
  • Keine Tests von Abhängigkeiten
  • Keine Nebeneffekte
     

Herausforderungen

Bei der Betrachtung von Klassen die Guice nutzen, wird die Zuordnung zu den verwendeten Implementierungen undurchsichtiger. Hier muss dann in das entsprechende Modul geschaut werden, in dem den Interfaces die konkrete Implementierung zugeordnet ist. Dies erschwert z.B. die Fehleranalyse, wenn Konzept und Framework nicht verstanden wurden.

Hmm..why should I care?

Bei der Konstruktion von Gebäuden ist die Einhaltung von Architekturvorgaben unverzichtbar. Genauso selbstverständlich sollten sich Softwareentwickler Prinzipien wie „Inversion of Control“ aneignen, um die Qualität der Software signifikant zu steigern. Eine losere Kopplung und eine bessere Testbarkeit dient nicht dem Selbstzweck, sondern führt zu einer verständlicheren, leichter erweiterbar und automatisiert testbarenr Software. Refactorings lassen sich dadurch kontrollierter durchführen. Dies resultiert in Systemen an denen länger Entwickelt nicht nur in einer Kostenersparnis, sondern erhöht die Kundenzufriedenheit signifikant.
 

Autoren: Robert Süggel u. Mihail Tsvyatkov