Was ist Dependency Injection?

Dieses Kapitel stellt die grundlegenden Programmierpraktiken vor, an die Sie sich beim Schreiben jeder Anwendung halten sollten. Es sind die Grundlagen, die für sauberen, verständlichen und wartbaren Code nötig sind.

Wenn Sie diese Regeln übernehmen und befolgen, wird Nette Sie auf Schritt und Tritt unterstützen. Es erledigt die Routinearbeit für Sie und bietet größtmöglichen Komfort, sodass Sie sich auf die eigentliche Logik konzentrieren können.

Die Prinzipien, die wir hier zeigen, sind ziemlich einfach. Es gibt nichts zu befürchten.

Erinnern Sie sich an Ihr erstes Programm?

Wir wissen nicht, in welcher Sprache Sie es geschrieben haben, aber wenn es PHP war, sah es wahrscheinlich ungefähr so aus:

function addition(float $a, float $b): float
{
	return $a + $b;
}

echo addition(23, 1); // gibt 24 aus

Ein paar triviale Zeilen Code, und doch stecken darin so viele zentrale Konzepte. Dass es Variablen gibt. Dass Code in kleinere Einheiten wie Funktionen unterteilt wird. Dass wir ihnen Eingabeargumente übergeben und sie Ergebnisse zurückgeben. Es fehlen nur Bedingungen und Schleifen.

Dass wir einer Funktion Eingabedaten übergeben und sie ein Ergebnis zurückgibt, ist ein völlig verständliches Konzept, das auch in anderen Bereichen verwendet wird, etwa in der Mathematik.

Eine Funktion hat ihre Signatur, die aus ihrem Namen, der Liste der Parameter samt Typen und schließlich dem Typ des Rückgabewerts besteht. Als Nutzer interessiert uns die Signatur; über die innere Implementierung müssen wir üblicherweise nichts wissen.

Stellen Sie sich nun vor, die Signatur der Funktion sähe so aus:

function addition(float $x): float

Eine Addition mit einem Parameter? Das ist seltsam… Und was ist damit?

function addition(): float

Das ist jetzt wirklich seltsam, oder? Wie wird die Funktion überhaupt verwendet?

echo addition(); // was gibt sie aus?

Bei solchem Code wären wir ratlos. Nicht nur ein Anfänger würde ihn nicht verstehen, sondern auch ein erfahrener Programmierer nicht.

Sie fragen sich, wie so eine Funktion innen wohl aussieht? Woher bekäme sie die Zahlen, die sie addieren soll? Wahrscheinlich würde sie sie sich irgendwie selbst beschaffen, etwa so:

function addition(): float
{
	$a = Input::get('a');
	$b = Input::get('b');
	return $a + $b;
}

Im Körper der Funktion haben wir versteckte Abhängigkeiten von anderen globalen Funktionen oder statischen Methoden entdeckt. Um herauszufinden, woher die Zahlen tatsächlich kommen, müssen wir weiter nachforschen.

So nicht!

Der eben gezeigte Entwurf ist der Inbegriff vieler negativer Eigenschaften:

  • Die Signatur der Funktion tat so, als bräuchte sie die zu addierenden Zahlen nicht, was uns verwirrt hat.
  • Wir haben keine Ahnung, wie wir die Funktion dazu bringen, zwei andere Zahlen zu addieren.
  • Wir mussten in den Code schauen, um herauszufinden, woher sie die Zahlen nimmt.
  • Wir haben versteckte Abhängigkeiten entdeckt.
  • Um alles zu verstehen, müssen wir auch diese Abhängigkeiten untersuchen.

Und ist es überhaupt die Aufgabe einer Additionsfunktion, sich die Eingaben zu beschaffen? Natürlich nicht. Ihre Verantwortung ist nur die Addition selbst.

Solchem Code wollen wir nicht begegnen, und schon gar nicht wollen wir ihn schreiben. Die Abhilfe ist einfach: zurück zu den Grundlagen und einfach Parameter verwenden:

function addition(float $a, float $b): float
{
	return $a + $b;
}

Regel Nr. 1: Lassen Sie es sich übergeben

Die wichtigste Regel lautet: Alle Daten, die Funktionen oder Klassen brauchen, müssen ihnen übergeben werden.

Statt versteckte Wege zu erfinden, auf denen sie sich die Daten selbst beschaffen, geben Sie ihnen einfach die Parameter. Sie sparen sich die Zeit, versteckte Pfade zu ersinnen, die Ihren Code ganz sicher nicht besser machen.

Wenn Sie diese Regel immer und überall befolgen, sind Sie auf dem Weg zu Code ohne versteckte Abhängigkeiten. Zu Code, der nicht nur für den Autor verständlich ist, sondern auch für jeden, der ihn später liest. Wo alles aus den Signaturen der Funktionen und Klassen hervorgeht und man keine versteckten Details in der Implementierung suchen muss.

Diese Technik heißt fachlich Dependency Injection. Und die Daten heißen Abhängigkeiten. Es ist bloß gewöhnliches Übergeben von Parametern, mehr nicht.

Bitte verwechseln Sie Dependency Injection, ein Entwurfsmuster, nicht mit einem “Dependency-Injection-Container”, der ein Werkzeug ist, also etwas grundlegend anderes. Über Container sprechen wir später.

Von Funktionen zu Klassen

Und wie überträgt sich das auf Klassen? Eine Klasse ist ein komplexeres Gebilde als eine einfache Funktion, aber Regel Nr. 1 gilt auch hier voll und ganz. Es gibt nur mehr Möglichkeiten, Argumente zu übergeben. Zum Beispiel ganz ähnlich wie bei der Funktion:

class Math
{
	public function sum(float $a, float $b): float
	{
		return $a + $b;
	}
}

$math = new Math;
echo $math->sum(23, 1); // 24

Oder über andere Methoden, oder direkt über den Konstruktor:

class Sum
{
	public function __construct(
		private float $a,
		private float $b,
	) {
	}

	public function calculate(): float
	{
		return $this->a + $this->b;
	}
}

$sum = new Sum(23, 1);
echo $sum->calculate(); // 24

Beide Beispiele entsprechen vollständig der Dependency Injection.

Beispiele aus dem echten Leben

In der realen Welt werden Sie keine Klassen zum Addieren von Zahlen schreiben. Gehen wir also zu praktischen Beispielen über.

Nehmen wir eine Klasse Article, die einen Blogartikel repräsentiert:

class Article
{
	public int $id;
	public string $title;
	public string $content;

	public function save(): void
	{
		// speichert den Artikel in der Datenbank
	}
}

und die Verwendung sieht so aus:

$article = new Article;
$article->title = '10 Dinge, die Sie über das Abnehmen wissen müssen';
$article->content = 'Jedes Jahr entscheiden sich Millionen Menschen ...';
$article->save();

Die Methode save() speichert den Artikel in einer Datenbanktabelle. Sie mit Nette Database umzusetzen wäre unkompliziert, gäbe es da nicht einen Haken: Woher bekommt Article die Verbindung zur Datenbank, also ein Objekt der Klasse Nette\Database\Connection?

Es scheint, als hätten wir viele Möglichkeiten. Sie könnte sie aus einer statischen Variablen nehmen. Oder durch Erben von einer Klasse, die die Datenbankverbindung bereitstellt. Oder ein Singleton verwenden. Oder sogenannte Facades, wie sie Laravel nutzt:

use Illuminate\Support\Facades\DB;

class Article
{
	public int $id;
	public string $title;
	public string $content;

	public function save(): void
	{
		DB::insert(
			'INSERT INTO articles (title, content) VALUES (?, ?)',
			[$this->title, $this->content],
		);
	}
}

Prima, das Problem ist gelöst.

Oder etwa nicht?

Erinnern wir uns an Regel Nr. 1: Lassen Sie es sich übergeben: Alle Abhängigkeiten, die eine Klasse braucht, müssen ihr übergeben werden. Denn wenn wir die Regel brechen, haben wir den Weg zu unübersichtlichem Code voller versteckter Abhängigkeiten eingeschlagen, und das Ergebnis wird eine Anwendung sein, deren Wartung und Weiterentwicklung eine Herausforderung ist.

Der Nutzer der Klasse Article hat keine Ahnung, wo die Methode save() den Artikel ablegt. In einer Datenbanktabelle? In welcher, der Produktions- oder der Testdatenbank? Und wie lässt sich das ändern?

Der Nutzer muss nachsehen, wie die Methode save() implementiert ist, und findet dort die Verwendung von DB::insert(). Er muss also weiter nachforschen, woher diese Methode die Datenbankverbindung bekommt. Und versteckte Abhängigkeiten können eine ziemlich lange Kette bilden.

In sauberem und gut entworfenem Code gibt es niemals versteckte Abhängigkeiten, Laravel-Facades oder statische Variablen. In sauberem und gut entworfenem Code werden Argumente übergeben:

class Article
{
	public function save(Nette\Database\Connection $db): void
	{
		$db->query('INSERT INTO articles', [
			'title' => $this->title,
			'content' => $this->content,
		]);
	}
}

Noch praktischer ist, wie wir später sehen werden, die Verwendung des Konstruktors:

class Article
{
	public function __construct(
		private Nette\Database\Connection $db,
	) {
	}

	public function save(): void
	{
		$this->db->query('INSERT INTO articles', [
			'title' => $this->title,
			'content' => $this->content,
		]);
	}
}

Wenn Sie ein erfahrener Programmierer sind, denken Sie vielleicht, dass Article überhaupt keine Methode save() haben sollte; sie sollte eine reine Datenstruktur sein, und das Speichern sollte ein eigenes Repository übernehmen. Das ergibt Sinn. Es würde uns aber weit über das Thema hinausführen, nämlich Dependency Injection, und über das Ziel, einfache Beispiele zu zeigen.

Wenn Sie eine Klasse schreiben, die für ihren Betrieb zum Beispiel eine Datenbank braucht, erfinden Sie nicht, woher Sie sie bekommen, sondern lassen Sie sie sich übergeben. Etwa als Parameter des Konstruktors oder einer anderen Methode. Bekennen Sie sich zu den Abhängigkeiten. Bekennen Sie sich in der API Ihrer Klasse dazu. Sie bekommen verständlichen und vorhersagbaren Code.

Und was ist mit dieser Klasse, die Fehlermeldungen protokolliert?

class Logger
{
	public function log(string $message): void
	{
		$file = LOG_DIR . '/log.txt';
		file_put_contents($file, $message . "\n", FILE_APPEND);
	}
}

Was meinen Sie, haben wir uns an Regel Nr. 1: Lassen Sie es sich übergeben gehalten?

Haben wir nicht.

Die entscheidende Information, das Verzeichnis mit der Logdatei, beschafft sich die Klasse selbst aus einer Konstanten.

Sehen Sie sich das Beispiel der Verwendung an:

$logger = new Logger;
$logger->log('Die Temperatur beträgt 23 °C');
$logger->log('Die Temperatur beträgt 10 °C');

Könnten Sie, ohne die Implementierung zu kennen, sagen, wohin die Meldungen geschrieben werden? Wäre Ihnen eingefallen, dass für den Betrieb die Konstante LOG_DIR existieren muss? Und könnten Sie eine zweite Instanz erzeugen, die woandershin schreibt? Sicher nicht.

Bringen wir die Klasse in Ordnung:

class Logger
{
	public function __construct(
		private string $file,
	) {
	}

	public function log(string $message): void
	{
		file_put_contents($this->file, $message . "\n", FILE_APPEND);
	}
}

Die Klasse ist jetzt viel verständlicher, konfigurierbarer und damit nützlicher.

$logger = new Logger('/path/to/log.txt');
$logger->log('Die Temperatur beträgt 15 °C');

Aber das interessiert mich nicht!

“Wenn ich ein Article-Objekt erzeuge und save() aufrufe, will ich mich nicht mit der Datenbank befassen; ich will nur, dass es in der gespeichert wird, die ich konfiguriert habe.”

“Wenn ich den Logger verwende, will ich nur, dass die Meldung geschrieben wird, und mich nicht damit befassen, wohin. Es soll die globale Einstellung gelten.”

Das sind berechtigte Einwände.

Zeigen wir als Beispiel eine Klasse, die Newsletter verteilt und das Ergebnis protokolliert:

class NewsletterDistributor
{
	public function distribute(): void
	{
		$logger = new Logger(/* ... */);
		try {
			$this->sendEmails();
			$logger->log('Die E-Mails wurden versendet');

		} catch (Exception $e) {
			$logger->log('Beim Versenden ist ein Fehler aufgetreten');
			throw $e;
		}
	}
}

Der verbesserte Logger, der die Konstante LOG_DIR nicht mehr verwendet, verlangt im Konstruktor den Pfad zur Datei. Wie lässt sich das lösen? Die Klasse NewsletterDistributor kümmert sich nicht darum, wohin die Meldungen geschrieben werden; sie will sie bloß protokollieren.

Die Lösung ist wieder Regel Nr. 1: Lassen Sie es sich übergeben: Wir übergeben alle Daten, die die Klasse braucht.

Heißt das also, dass wir den Pfad zum Log durch den Konstruktor übergeben und ihn dann beim Erzeugen des Logger-Objekts verwenden?

class NewsletterDistributor
{
	public function __construct(
		private string $file, // ⛔ NICHT SO!
	) {
	}

	public function distribute(): void
	{
		$logger = new Logger($this->file);

Nicht so! Denn der Pfad ist nicht ein Datum, das die Klasse NewsletterDistributor braucht; ihn braucht der Logger. Sehen Sie den Unterschied? Die Klasse NewsletterDistributor braucht den Logger selbst. Also übergeben wir den Logger selbst:

class NewsletterDistributor
{
	public function __construct(
		private Logger $logger, // ✅
	) {
	}

	public function distribute(): void
	{
		try {
			$this->sendEmails();
			$this->logger->log('Die E-Mails wurden versendet');

		} catch (Exception $e) {
			$this->logger->log('Beim Versenden ist ein Fehler aufgetreten');
			throw $e;
		}
	}
}

Jetzt geht aus der Signatur der Klasse NewsletterDistributor hervor, dass das Protokollieren Teil ihrer Aufgabe ist. Und den Logger durch einen anderen zu ersetzen, etwa für Tests, ist völlig unkompliziert. Sollte sich außerdem der Konstruktor der Klasse Logger ändern, hat das auf unsere Klasse keinerlei Auswirkung.

Regel Nr. 2: Nehmen Sie, was Ihnen gehört

Lassen Sie sich nicht verwirren und übernehmen Sie nicht die Abhängigkeiten Ihrer Abhängigkeiten. Nehmen Sie nur Ihre eigenen.

Dadurch ist Code, der andere Objekte verwendet, völlig unabhängig von Änderungen an deren Konstruktoren. Seine API wird genauer. Und vor allem lassen sich diese Abhängigkeiten mühelos durch andere ersetzen.

Neues Familienmitglied

Das Entwicklungsteam hat beschlossen, einen zweiten Logger zu bauen, der in die Datenbank schreibt. Wir erzeugen also eine Klasse DatabaseLogger. Jetzt haben wir zwei Klassen, Logger und DatabaseLogger; die eine schreibt in eine Datei, die andere in die Datenbank … wirkt die Benennung nicht etwas seltsam? Wäre es nicht besser, Logger in FileLogger umzubenennen? Sicherlich.

Aber machen wir es geschickt. Wir legen ein Interface mit dem ursprünglichen Namen an:

interface Logger
{
	function log(string $message): void;
}

… das beide Logger implementieren werden:

class FileLogger implements Logger
// ...

class DatabaseLogger implements Logger
// ...

Und dadurch muss im übrigen Code, der den Logger verwendet, nichts geändert werden. Der Konstruktor der Klasse NewsletterDistributor etwa wird sich weiterhin damit begnügen, Logger als Parameter zu verlangen. Und es liegt an uns, welche Instanz wir ihm geben.

Deshalb hängen wir an Namen von Interfaces nie das Suffix Interface und stellen ihnen nie das Präfix I voran. Sonst ließe sich der Code nicht so elegant erweitern.

Houston, wir haben ein Problem

Während wir in der gesamten Anwendung mit einer einzigen Instanz des Loggers auskommen, ob dateibasiert oder datenbankbasiert, und sie einfach überall dorthin übergeben, wo protokolliert wird, sieht es bei der Klasse Article ganz anders aus. Ihre Instanzen erzeugen wir nach Bedarf, auch mehrfach. Wie gehen wir mit der Datenbank-Abhängigkeit in ihrem Konstruktor um?

Ein Beispiel könnte ein Controller sein, der nach dem Absenden eines Formulars einen Artikel in der Datenbank speichern soll:

class EditController extends Controller
{
	public function formSubmitted($data)
	{
		$article = new Article(/* ... */);
		$article->title = $data->title;
		$article->content = $data->content;
		$article->save();
	}
}

Eine mögliche Lösung scheint auf der Hand zu liegen: Lassen wir das Datenbankobjekt über den Konstruktor an EditController übergeben und verwenden $article = new Article($this->db).

Genau wie im vorigen Fall mit Logger und dem Pfad zur Datei ist das nicht der richtige Weg. Die Datenbank ist keine Abhängigkeit von EditController, sondern von Article. Sie zu übergeben verstößt also gegen Regel Nr. 2: Nehmen Sie, was Ihnen gehört. Ändert sich der Konstruktor der Klasse Article (kommt ein neuer Parameter hinzu), müssen Sie den Code an allen Stellen anpassen, an denen Instanzen erzeugt werden. Uff.

Houston, was schlagen Sie vor?

Regel Nr. 3: Überlassen Sie es der Factory

Indem wir versteckte Abhängigkeiten beseitigt und alle Abhängigkeiten als Argumente übergeben haben, haben wir besser konfigurierbare und flexiblere Klassen gewonnen. Deshalb brauchen wir noch etwas, das uns diese flexibleren Klassen erzeugt und konfiguriert. Wir nennen es Factories.

Die Regel lautet: Hat eine Klasse Abhängigkeiten, überlassen Sie das Erzeugen ihrer Instanzen einer Factory.

Factories sind in der Welt der Dependency Injection die klügere Alternative zum Operator new.

Bitte verwechseln Sie das nicht mit dem Entwurfsmuster Factory Method, das eine bestimmte Art beschreibt, Factories zu nutzen, und mit diesem Thema nichts zu tun hat.

Factory

Eine Factory ist eine Methode oder Klasse, die Objekte erzeugt und konfiguriert. Die Klasse, die Article herstellt, nennen wir ArticleFactory, und sie könnte so aussehen:

class ArticleFactory
{
	public function __construct(
		private Nette\Database\Connection $db,
	) {
	}

	public function create(): Article
	{
		return new Article($this->db);
	}
}

Ihre Verwendung im Controller sieht so aus:

class EditController extends Controller
{
	public function __construct(
		private ArticleFactory $articleFactory,
	) {
	}

	public function formSubmitted($data)
	{
		// die Factory das Objekt erzeugen lassen
		$article = $this->articleFactory->create();
		$article->title = $data->title;
		$article->content = $data->content;
		$article->save();
	}
}

Ändert sich nun die Signatur des Konstruktors der Klasse Article, ist die ArticleFactory der einzige Teil des Codes, der darauf reagieren muss. Aller übrige Code, der mit Article-Objekten arbeitet, etwa EditController, bleibt davon unberührt.

Vielleicht kratzen Sie sich jetzt am Kopf und fragen sich, ob wir die Lage überhaupt verbessert haben. Die Menge an Code ist gewachsen, und das Ganze fängt an, verdächtig kompliziert auszusehen.

Keine Sorge, wir kommen bald zum Nette DI Container. Und der hat einige Tricks auf Lager, die das Bauen von Anwendungen mit Dependency Injection erheblich vereinfachen. Statt der Klasse ArticleFactory genügt dann zum Beispiel, bloß ein Interface zu schreiben:

interface ArticleFactory
{
	function create(): Article;
}

Aber wir greifen vor, bleiben Sie dran :-)

Zusammenfassung

Am Anfang dieses Kapitels haben wir versprochen, ein Vorgehen für den Entwurf sauberen Codes zu zeigen. Sorgen Sie einfach dafür, dass Klassen:

  1. die Abhängigkeiten übergeben bekommen, die sie brauchen
  2. und umgekehrt nicht das übergeben bekommen, was sie nicht unmittelbar brauchen
  3. und dass Objekte mit Abhängigkeiten am besten in Factories erzeugt werden

Auf den ersten Blick mag es nicht so wirken, aber diese drei Regeln haben weitreichende Folgen. Sie führen zu einer radikal anderen Sicht auf den Entwurf von Code. Lohnt sich das? Programmierer, die alte Gewohnheiten abgelegt und angefangen haben, Dependency Injection konsequent zu nutzen, halten diesen Schritt für einen Wendepunkt in ihrer beruflichen Laufbahn. Er hat ihnen eine Welt übersichtlicher und wartbarer Anwendungen eröffnet.

Und was, wenn der Code Dependency Injection nicht konsequent nutzt? Was, wenn er auf statischen Methoden oder Singletons aufbaut? Führt das zu Problemen? Ja, und zwar zu sehr erheblichen.

Version: 3.x