Dependency Injection: Ist Setter-Injection wirklich nötig?

16.04.2011

Jeder Software-Entwickler kennt verschiedene Arten von Dependency Injection, die beiden Bekanntesten dürften Constructor Injection für benötigte und Setter Injection für optionale Abhängigkeiten sein.

Betrachten wir ein Beispiel. Wir haben die Implementierung eines Client-Interfaces, diese hat eine optionale Abhängigkeit zu einer Dependency. Der folgende Code ist PHP, ist aber äquivalent in anderen Sprachen möglich. Hier sind die Schnittstellen:

/**
* Something the Client may depend on or not.
*/
interface Dependency{
	public function action();}

/**
* The Client.
*/
interface Client{
	public function doSomething();}

Die Implementierung von Dependency sieht so aus:

/**
* Implementation of the dependency which will do something.
*/
class UsefulDependencyImplementation implements Dependency{
	public function action(){
		/* Do something. */}}

Eine konkrete Client mit optionaler Abhängigkeit zu Dependency sieht so aus (die wichtigen Teile sind markiert):

/**
* Client implementation using setter injection. The dependency may or may not
* be injected.
*/
class ClientImplementationWithOptionalDependency implements Client{
	private $dep;

	public function setDependency(Dependency $dep){
		$this->dep = $dep;}

	public function doSomething(){
		/* Some client logic. */
		if (isset($this->dep)){
			$this->dep->action();}
		/* Some client logic. */}}

Client-Objekte werden dann entsprechend erzeugt:

// Create client using the dependency.
$client = new ClientImplementationWithOptionalDependency();
$client->setDependency(new UsefulDependencyImplementation());

// Create client not using the dependency.
$client = new ClientImplementationWithOptionalDependency();

Diese Form der optionalen Abhängigkeit hat jedoch einige Nachteile für die Client-Klasse:

  • Es kann passieren, dass Objekte erzeugt werden, die ein Dependency-Objekt benutzen sollten, dies aber nicht tun. Da die Clients per Design auch ohne arbeiten können, fällt das nicht auf.
  • Die Client-Klasse erhält eine zusätzliche öffentliche Methode, nämlich den Setter für die Abhängigkeit. Ihre Schnittstelle wird also größer. Das sollte normalerweise kein Problem sein, wirkt aber zumindest unschön.
  • Alle Methoden in Client, die die Dependency benutzen, müssen einen entsprechenden Check durchführen, ob die Abhängigkeit gesetzt ist oder nicht.
  • Beim Erstellen von Unit Tests ist zu beachten, dass beide Fälle abgedeckt sein müssen – Client muss sowohl mit als auch ohne Dependency funktionieren.

Die Lösung dieser Probleme liegt im Null Object Pattern. Es wird eine zusätzliche Implementierung von Dependency erstellt, die nichts tut:

/**
* Null implementation of the dependency. Does nothing.
*/
class DependencyNullImplementation implements Dependency{
	public function action(){}}

Die Client-Implementierung wird entsprechend mit einer benötigten Abhängigkeit ausgestattet:

/**
* Client implementation which needs always an object satisfying the
* Dependency's interface.
*/
class ClientImplementationWithMandatoryDependency implements Client{
	private $dep;

	public function __construct(Dependency $dep){
		$this->dep = $dep;}

	public function doSomething(){
		/* Some client logic. */
		$this->dep->action();
		/* Some client logic. */}}

Instanzen werden nun so erzeugt:

// Create client using the dependency.
$client = new ClientImplementationWithMandatoryDependency(new UsefulDependencyImplementation());

// Create client not using the dependency.
$client = new ClientImplementationWithMandatoryDependency(new DependencyNullImplementation());

Die oben genannten Nachteile wurden neutralisiert:

  • Es ist nicht möglich, die Abhängigkeit zu vergessen, nun muss man sie wenigstens (mit der Null-Implementierung) verwechseln, um den gleichen Fehler zu begehen.
  • Statt einer öffentlichen Setter-Methode ist Dependency in den Konstruktor gewandert. Das hat in diesem Fall nichts gespart, würde aber spätestens dann zu weniger Code führen, wenn es andere benötigte Abhängigkeiten gibt und sowieso ein Konstruktor vorhanden wäre. Auf jeden Fall gibt es keine zusätzliche öffentliche „normale“ Methode.
  • Die Client-Methoden können bedenkenlos die Abhängigkeit nutzen, ihre Existenz ist garantiert. Dadurch wird weniger Code benötigt und die Klasse wird einfacher.
  • Der Unit-Test, der Client ohne Dependency prüft, kann weggelassen werden.

Natürlich hat diese Variante nicht nur Vorteile, einige Dinge sind zu bedenken:

  • Die Implementierung ist weniger flexibel, da die Abhängigkeit nicht mehr zur Laufzeit geändert werden kann. Das ist jedoch kein wirklicher Nachteil, weil Abhängigkeiten nicht zum eigentlichen Zustand eines Objektes gehören (wie das bei anderen Instanzvariablen der Fall ist) und normalerweise bleiben, wie sie sind (in Java beispielsweise werden sie als final markiert).
  • Für die Null-Implementierung werden Unit Tests benötigt. Auch das ist wenig problematisch, da die Klasse sehr simpel ist und auch die Tests entsprechend einfach ausfallen.

In diesem Beispiel ist natürlich die Null-Implementierung von Dependency extrem simpel, falls Client Rückgabewerte erwarten würde oder komplexere Aktionen durchführen würde, wäre sie natürlich umfangreicher, eventuell wären sogar weitere Klassen vonnöten. Dabei würde es sich allerdings nur um eine Verschiebung der Komplexität handeln, die sich bis dato in Client befinden würde.

Alternativ wäre natürlich statt der Verwendung des Null Object Patterns auch möglich, zwei Implementierungen von Client zu erstellen, eine mit Abhängigkeit, eine ohne. Dabei gibt es natürlich schnell das Problem duplizierten Codes, der eventuell ausgelagert werden müsste, was wiederum Boilerplate-Code nach sich zieht.

Der gesamte Beispielcode als Gist: Verschiedene Implementierungen einer Client-Klasse mit einer optionalen Abhängigkeit