Einführung in das Dependency Injection Pattern

veröffentlicht am 02. Juni 2011

Heute ist mir mal wieder nach einem etwas längeren Programmier-Artikel. Und zwar gebe ich euch eine kurze Einführung in das Dependency Injection Pattern. Zwar stelle ich das Dependency Injection Pattern am Beispiel mit Java dar, es lässt sich jedoch auch in beliebigen anderen objektorientierten Programiersprachen umsetzen.

Ausgangslage

In objektorientierten Programmiersprachen stehen viele Objekte in Beziehung zueinander. Benötigt zum Beispiel Klasse A eine Funktionalität der Klasse B, so ist es naheliegend, in der Klasse A ein Objekt der Klasse B zu erzeugen, um dann auf die Methoden der Klasse B zugreifen zu können. Das Problem daran ist, dass zwischen den beiden Klassen eine starke Bindung entsteht.

Etwas bildlicher ist vielleicht folgendes Beispiel. Eure Anwendung greift auf eine MySQL-Datenbank zurück und eure Datenbank-Klasse verwendet verschiedene MySQL-spezifische Befehle. Auf einmal kommt die Anforderung auf, dass eure Anwendung auch eine MSSQL-Datenbank unterstützen muss. Jetzt müsst ihr eure Anwendung (die auf die Datenbank-Klasse zurückgreift) aufwändig anpassen und ggf. große Codeteile duplizieren. Besser wäre es, wenn man die Datenbank-Implementierung einfach austauschen könnte. An dieser Stelle kommt Dependency Injection ins Spiel.

Beispiel mit starker Bindung

Zunächst wird ein einfacher Logger implementiert:

public class ConsoleLogger {

	public void append(String message) {
		System.out.println(message);
	}

}

Dieser Logger wird von einer anderen Klasse verwendet:

public class AnyClass {

	private ConsoleLogger logger = new ConsoleLogger();

	public void doSomething() {
		logger.append("I do something");
	}

}

Die folgende Klasse führt dann die Anwendung aus:

public class Main {

	public static void main(String[] args) {
		AnyClass any = new AnyClass();
		any.doSomething();
	}

}

Das Beispiel ist jetzt recht einfach gestrickt. Wenn man sich aber vorstellt, dass eine Vielzahl an Klassen den Logger verwenden und nun auch die Möglichkeit angeboten werden soll, die Log-Meldungen in eine Datei zu schreiben, müssten alle Klassen angepasst werden, die den Logger verwenden.

Und nun kommt das Dependency Injection Pattern ins Spiel. Dabei werden die Abhängigkeiten einer Klasse nicht von der Klasse selbst instanziiert, sondern über eine weitere Klasse zugeteilt (injiziert).

Beispiel mit Dependency Injection

Zunächst wird ein Interface erstellt, das die Schnittstellen für alle möglichen Logger-Implementierungen vorgibt:

public interface ILogger {

	void append(String message);

}

Der folgende Logger macht dasselbe wie der Logger aus dem vorigen Beispiel. Der einzige Unterschied besteht darin, dass er das Logger-Interface implementiert.

public class ConsoleLogger implements ILogger {

	public void append(String message) {
		System.out.println(message);
	}

}

Jetzt wird wieder eine Klasse implementiert, die Logging-Funktionalitäten nutzen soll. Wie man sieht, erzeugt die Klasse nicht mehr selber ein Objekt des Loggers, sondern stellt eine Methode bereit, über die ihr ein Logger zugewiesen werden kann:

public class AnyClass {

	private ILogger logger;

	public void setLogger(ILogger logger) {
		this.logger = logger;
	}

	public void doSomething() {
		logger.append("I do something");
	}

}

Jetzt benötigt man noch eine Klasse, die sich um die Erzeugung und Zuweisung der Abhängigkeiten kümmert:

public class DependencyInjector {

	public static void main(String[] args) {
		ILogger logger = new ConsoleLogger();
		AnyClass any = new AnyClass();
		any.setLogger(logger);
		any.doSomething();
	}

}

In diesem Beispiel wird natürlich wieder nur einer Klasse die Abhängigkeit zugewiesen, dementsprechend wäre der Aufwand für die Anpassung an eine andere Logger-Implementierung auch noch ohne Dependency Injection relativ einfach.

Möchte man nun aber auch eine andere Logger-Implementierung unterstützen, muss nur an dieser einen zentralen Stelle die konkrete Implementierung ausgetauscht werden. In der Praxis würde man noch weiter gehen und z.B. über eine properties-Datei die zu wählende Logging-Implementierung festlegen, sodass eine Anpassung der Klasse, die sich um die Zuweisung der Abhängigkeiten kümmert, nicht nötig ist.

Kommentare

Netter Artikel, auch wenn gerade Logging häufig nicht injeziert wird.
Was man noch erwähnen könnte ist, dass auch wenn man nur die Klasse ConsoleLogger benutzt (ohne das Interface), DI auch Sinn macht, da dann beim hinzufügen einer weiteren Loggerart das refactoring wesentlich einfacher wird. Im Idealfall kostet es dann sogar fast keinen Aufwand, da die einzige bisher existierende Klasse wie das Interface heißt, dann braucht man wirklich nur das erzeugen der Interface-Instanz zu ändern.

Ich hoffe man versteht noch, was ich meine, wenn nicht, werde ich wohl mal selber einen Blogartikel dazu schreiben.

Kommentar #1 von Michael Vitz am 02. Juni 2011


@Michael Vitz: Ich verstehe nicht so recht, was du meinst.

Wenn du in der Klasse, die den Logger verwenden soll, nicht gegen ein Interface oder eine Oberklasse programmierst, kannst du die Implementierung nicht einfach austauschen. Dann wäre das in etwa derselbe Refactoring-Aufwand wie ohne DI.

Natürlich kannst du einfach von der oben genannten ConsoleLogger-Klasse ableiten, die Methode append überschreiben und dann in der DependencyInjector-Klasse einfach ein Objekt der Unterklasse erzeugen. Ist aber nicht sehr sauber :)

Kommentar #2 von Patrick am 02. Juni 2011


Was ich sagen wollte ist, dass http://pastebin.com/aQWSvstt auch schon besser ist, als dein Anfangszustand, auch wenn wir natürlich noch sehr stark gekoppelt sind.

Kommt später eine weitere Implementierung hinzu, müssen wir „nur“ folgendes machen:
1. Aus Logger ein Interface machen (kann und sollte den Namen Logger behalten)
2. Den bisherigen Methodenrumpf aus Logger in eine neue Klasse packen, welche Logger implementiert.
3. Die Stelle an der der Logger bisher erzeugt wird anpassen.
Wie man sieht, brauchen wir dann die Klasse AnyClass nicht anpacken.

Das Ergebnis danach ist dann eben Identisch zu deinem Endzustand: http://pastebin.com/nmsa6vxK

Hoffe das es jetzt besser verständlich ist.

Kommentar #3 von Michael Vitz am 02. Juni 2011


Ach nun verstehe ich :)
Klar, ist machbar, aber besser von Anfang an mit API + Impl arbeiten ;)

Zudem muss man „Glück haben“, den Klassennamen recht allgemein formuliert zu haben. Solange der komplette Quellcode in der eigenen Hand liegt, wäre das noch nicht so schlimm, mit einer modernen IDE kann man dann einfach den Namen austauschen.

Sobald aber auch andere Personen Plugins o.ä. für das System entwickeln, ist es recht kritisch, den Namen auszutauschen :)

Kommentar #4 von Patrick am 02. Juni 2011


Klar, generell gebe ich dir da Recht, ABER ;)

Bezüglich Generalität der Namen: Meiner Erfahrung nach ist es meistens so, dass die Namen solange es nur genau eine Implementierung gibt sowieso einen sehr generellen Namen haben, da es nur wenig Sinn macht einen sehr speziellen Namen zu vergeben. Beispiel: Wenn es nur einen Logger gibt, wieso sollte man den dann ConsoleLogger nennen? Es gibt nur einen Logger und der logt, also nenne ich die Klasse Logger (auch wenn der dann auf die Konsole loggt).

Dazu kommt, dass ich einfach Verfechter von Pragmatismus bin. Es macht einfach keinen Sinn für jede Klasse ein Interface zu erstellen nur um später flexibel zu sein, sondern man sollte das erst dann machen, wenn man mehr als eine Implementierung hat, das macht auch eine Architektur wesentlich verständlicher und bläht sie nicht unnötig auf.

Aber generell kann man hier sagen, dass es wie so oft keine „perfekte“ Lösung gibt, sondern man von Fall zu Fall gucken kann und auch sollte. Jedenfalls ist DI (ob jetzt mit oder ohne Interface) wesentlich besser als der oben skizzierte Anfangszustand!

Kommentar #5 von Michael Vitz am 02. Juni 2011


Natürlich gebe ich dir recht, dass man nicht für jedes Objekt ein Interface schreiben sollte, sondern nur für Objekte, bei denen es Sinn macht :)

Hast du ein eigenes Blog, oder wie kann ich deinen ersten Kommentar verstehen?

Ich hoffe man versteht noch, was ich meine, wenn nicht, werde ich wohl mal selber einen Blogartikel dazu schreiben.

Kommentar #6 von Patrick am 02. Juni 2011


Zur Zeit nicht, aber ich denke immer mal wieder nach ein wenig über Java zu bloggen und evtl. wäre das eine Gelegenheit gewesen mal wieder einen Blog zu installieren.

Kommentar #7 von Michael Vitz am 02. Juni 2011


Endlich mal wieder Java-Themen :D
Nicht nur das Anpassen ist ein riesen Problem bei dem ersten Beispiel, sondern auch das Testen dieser Klasse wird zu einem Problem (mocking etc.).

Setter-Injections finde ich aber persönlich nicht gut. Gerade wenn man auf die Beziehung angewiesen ist, finde ich sollte man Constructor-Injection vorziehen.
Hat den Vorteil man sieht direkt beim Instanziieren welche Abhängigkeiten bestehen! Ich sehe direkt beim Benutzen deiner Klasse, dass ich auch eine Instanz von ILogger brauche! Das sehe ich bei der setter-Variante nicht
Wäre die Klasse jetzt nicht so überschaubar und ich würde deine API nutzen (und nicht evtl. die API Doc lesen wollen :D ), würde ich vermutlich beim ersten Ausführen eine NPE bekommen :)

Kann nur den clean code talk von Misko Hevery empfehlen dazu: http://www.youtube.com/watch?v=RlfLCWKxHJ0

Gerade sein Haus-Beispiel spiegelt das wieder was ich gerade meinte :)

Kommentar #8 von Basti am 02. Juni 2011


Ich habe mich auch erst vor kurzem mit einem unserer erfahrenen Architekten über Setter- vs. Constructor-Injection im Zusammenhang mit Spring unterhalten.

Ich hatte zuerst argumentiert, dass man mit dem Konstruktor ja erzwingen kann, dass eine entsprechende Referenz übergeben werden muss. Sein Argument, dass man einfach null übergeben kann, hat dann gezogen. Zudem sind Setter-Injections bei Spring einfacher zu handhaben, als Constructor-Injections.

Wenn man ohne Frameworks wie Spring arbeitet, würde ich natürlich auch zu Constructor-Injection bei notwendigen Abhängigkeiten tendieren.

Kommentar #9 von Patrick am 02. Juni 2011


„Sein Argument, dass man einfach null übergeben kann, hat dann gezogen“

Ähm, das ist ein ziemlich schlechtes Argument! Da fande ich meine Argumente um einiges besser :)

http://misko.hevery.com/2009/02/19/constructor-injection-vs-setter-injection/

Bitte lese dir das Beispiel mit dem CreditCardProcessor durch! Genau das meinte ich, du siehst während der Benutzung schon, was notwendig ist um die API korrekt zu benutzen, bei der setter-Variante würdest du evtl. zig mal zu deinem Kollegen rennen(der die anderen Klassen implementiert hat) und nachfragen, hey XYZ, wieso erhalte ich hier die und die Exception.

Aber klar, man kann sich da jetzt wieder drum streiten, jedenfalls ist beides besser als die Instanziierung innerhalb des Konstruktors zu tätigen!

Kommentar #10 von Basti am 02. Juni 2011


@Basti: Ich meinte das Beispiel mit null nur in Bezug auf Spring. Im Vergleich zur Constructor-Injection ist die Setter-Injection unter Spring komfortabler.

Beispiel Setter-Injection

<bean id="address" class="demo.Address">
   <property name="street" value="SampleStreet"/>
</bean>

Beispiel Konstruktor-Injection

<bean id="address" class="demo.Address">
   <constructor -arg index="0" type="java.lang.String" value="SampleStreet"/>
</bean>

Aber natürlich hat man auch bei Spring bei der Constructor-Injection den Vorteil, dass es sofort knallt, wenn man vergessen hat, eine notwendige Abhängigkeit per Konstruktor zu setzen.

Kommentar #11 von Patrick am 02. Juni 2011


Ok kann sein, hier gings ja aber um die Benutzung ohne Frameworks. Ansonsten benutze ich eh nur guice, da spielts keine Rolle :P
Und auch in Spring gibts doch mittlerweile Annotation-Configuration…XML-Config. finde ich persönlich eh häßlich :)

Kommentar #12 von Basti am 02. Juni 2011


Hmm Annotations statt XML-Files in Spring… muss ich mir unbedingt mal anschauen, habe bisher nur relativ wenig mit Spring gemacht.

Kommentar #13 von Patrick am 02. Juni 2011


@Basti: ich habe mir gerade mal das Video von Miško Hevery reingezogen. Sehr interessant, aber ich glaube das muss ich nochmal gucken, so spät abends konnte ich mir das nicht alles merken :)

Kommentar #14 von Patrick am 03. Juni 2011


Hallo Patrick,

das nenn‘ ich mal eine gut verständliche, anschauliche Einführung in DI.
War genau das richtige in meiner Situation und ein wichtiger Meilenstein auf meinem Weg, die Vorzüge von Spring zu erfassen.
Danke :)

Kommentar #15 von Roman am 11. September 2014


Hallo und vielen Dank für deinen Artikel. Danke für die verständliche Formulierung und Veranschaulichung. Im Grunde ist es ganz einfach, aber viele machen es zu einer recht komplizierten Sache. Ich hatte mal ein ähnliches Szenario in C# und musste auch mit übergebenem Objekt arbeiten. Da kannte ich allerdings DI und IoC noch nicht.

Kommentar #16 von phoenix1984 am 30. April 2015


Hinterlasse einen Kommentar