Was ist Dependency Injection?
In diesem Kapitel werden Sie mit den grundlegenden Programmierpraktiken vertraut gemacht, die Sie beim Schreiben jeder Anwendung befolgen sollten. Dies sind die Grundlagen, die zum Schreiben von sauberem, verständlichem und wartbarem Code erforderlich sind.
Wenn Sie diese Regeln lernen und befolgen, wird Nette Ihnen bei jedem Schritt zur Seite stehen. Es wird Routineaufgaben für Sie erledigen und Ihnen maximalen Komfort bieten, so dass Sie sich auf die eigentliche Logik konzentrieren können.
Die Prinzipien, die wir hier zeigen, sind ganz einfach. Sie brauchen sich um nichts zu kümmern.
Erinnern Sie sich an Ihr erstes Programm?
Wir wissen nicht, in welcher Sprache Sie es geschrieben haben, aber wenn es PHP war, könnte es etwa so aussehen:
function summe(float $a, float $b): float
{
return $a + $b;
}
echo summe(23, 1); // gibt 24 aus
Ein paar triviale Codezeilen, aber so viele wichtige Konzepte, die darin versteckt sind. Dass es Variablen gibt. Dass der Code in kleinere Einheiten unterteilt ist, die zum Beispiel Funktionen sind. Dass wir ihnen Eingabeargumente übergeben und sie Ergebnisse zurückgeben. Alles, was fehlt, sind Bedingungen und Schleifen.
Die Tatsache, dass eine Funktion Eingabedaten entgegennimmt und ein Ergebnis zurückliefert, ist ein durchaus verständliches Konzept, das auch in anderen Bereichen, z. B. der Mathematik, verwendet wird.
Eine Funktion hat ihre Signatur, die aus ihrem Namen, einer Liste von Parametern und deren Typen und schließlich dem Typ des Rückgabewerts besteht. Als Benutzer sind wir an der Signatur interessiert und müssen normalerweise nichts über die interne Implementierung wissen.
Stellen Sie sich nun vor, die Funktionssignatur sähe wie folgt aus:
function summe(float $x): float
Ein Zusatz mit einem Parameter? Das ist seltsam… Und was ist damit?
function summe(): float
Das ist doch wirklich seltsam, oder? Wie wird die Funktion verwendet?
echo summe(); // was wird gedruckt?
Wenn wir uns einen solchen Code ansehen, wären wir verwirrt. Nicht nur ein Anfänger würde ihn nicht verstehen, sondern auch ein erfahrener Programmierer würde einen solchen Code nicht verstehen.
Fragen Sie sich, wie eine solche Funktion eigentlich aussehen würde? Woher würde sie die Summanden bekommen? Sie würde sie wahrscheinlich irgendwie selbst beschaffen, vielleicht so:
function summe(): float
{
$a = Input::get('a');
$b = Input::get('b');
return $a + $b;
}
Es stellt sich heraus, dass es versteckte Bindungen zu anderen Funktionen (oder statischen Methoden) im Körper der Funktion gibt, und um herauszufinden, woher die Summanden tatsächlich kommen, müssen wir weiter graben.
Nicht hier entlang!
Das eben gezeigte Design ist die Essenz vieler negativer Merkmale:
- die Funktionssignatur gibt vor, dass sie die Summanden nicht braucht, was uns verwirrt
- wir haben keine Ahnung, wie wir die Funktion mit zwei anderen Zahlen rechnen lassen können
- wir mussten uns den Code ansehen, um herauszufinden, woher die Summanden kamen
- wir haben versteckte Abhängigkeiten gefunden
- ein vollständiges Verständnis erfordert auch die Untersuchung dieser Abhängigkeiten
Und ist es überhaupt die Aufgabe der Additionsfunktion, Eingaben zu beschaffen? Nein, natürlich nicht. Ihre Aufgabe ist es nur, zu addieren.
Solchen Code wollen wir nicht sehen, und wir wollen ihn schon gar nicht schreiben. Die Abhilfe ist einfach: Zurück zu den Grundlagen und einfach Parameter verwenden:
function summe(float $a, float $b): float
{
return $a + $b;
}
Regel Nr. 1: Lass es dir übergeben
Die wichtigste Regel lautet: alle Daten, die Funktionen oder Klassen benötigen, müssen an sie übergeben werden.
Anstatt versteckte Wege für den Zugriff auf die Daten selbst zu erfinden, übergeben Sie einfach die Parameter. So sparen Sie Zeit, die Sie sonst für das Erfinden versteckter Pfade aufwenden müssten, die Ihren Code sicherlich nicht verbessern würden.
Wenn Sie diese Regel immer und überall befolgen, sind Sie auf dem Weg zu einem Code ohne versteckte Abhängigkeiten. Zu einem Code, der nicht nur für den Autor, sondern auch für jeden, der ihn später liest, verständlich ist. Wo alles aus den Signaturen von Funktionen und Klassen verständlich ist und man nicht nach versteckten Geheimnissen in der Implementierung suchen muss.
Diese Technik wird in der Fachsprache dependency injection genannt. Und diese Daten werden Abhängigkeiten genannt. Es ist nur eine gewöhnliche Parameterübergabe, nichts weiter.
Verwechseln Sie bitte nicht Dependency Injection, die ein Entwurfsmuster ist, mit einem „Dependency Injection Container“, der ein Werkzeug ist, etwas diametral anderes. Wir werden uns später mit Containern beschäftigen.
Von Funktionen zu Klassen
Und wie hängen die Klassen zusammen? Eine Klasse ist eine komplexere Einheit als eine einfache Funktion, aber auch hier gilt Regel Nr. 1 uneingeschränkt. Es gibt einfach mehr Möglichkeiten, Argumente zu übergeben. Zum Beispiel, ganz ähnlich wie im Fall einer Funktion:
class Mathematik
{
public function summe(float $a, float $b): float
{
return $a + $b;
}
}
$math = new Mathematik;
echo $math->summe(23, 1); // 24
Oder durch andere Methoden, oder direkt durch den Konstruktor:
class Summe
{
public function __construct(
private float $a,
private float $b,
) {
}
public function calculate(): float
{
return $this->a + $this->b;
}
}
$summe = new Summe(23, 1);
echo $summe->calculate(); // 24
Beide Beispiele stehen vollständig im Einklang mit Dependency Injection.
Beispiele aus der Praxis
In der realen Welt werden Sie keine Klassen für die Addition von Zahlen schreiben. Kommen wir nun zu den praktischen Beispielen.
Nehmen wir eine Klasse Article
, die einen Blogbeitrag darstellt:
class Article
{
public int $id;
public string $title;
public string $content;
public function save(): void
{
// speichert den Artikel in der Datenbank
}
}
und die Verwendung wird wie folgt sein:
$article = new Article;
$article->title = '10 Things You Need to Know About Losing Weight';
$article->content = 'Every year millions of people in ...';
$article->save();
Die Methode save()
speichert den Artikel in einer Datenbanktabelle. Die Implementierung mit Nette Database ist ein Kinderspiel, wenn es nicht ein Problem gäbe: Woher bekommt
Article
die Datenbankverbindung, d.h. ein Objekt der Klasse Nette\Database\Connection
?
Es scheint, dass wir viele Möglichkeiten haben. Es kann die Verbindung von einer statischen Variable irgendwoher nehmen. Oder von einer Klasse erben, die eine Datenbankverbindung bereitstellt. Oder die Vorteile eines Singletons nutzen. Oder sogenannte Fassaden verwenden, die in Laravel verwendet werden:
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],
);
}
}
Toll, wir haben das Problem gelöst.
Oder haben wir das?
Erinnern wir uns an Regel Nr. 1: „Let It Be Passed to You“: Alle Abhängigkeiten, die die Klasse benötigt, müssen an sie weitergegeben werden. Denn wenn wir diese Regel brechen, haben wir uns auf einen Weg zu schmutzigem Code voller versteckter Abhängigkeiten und Unverständlichkeit begeben, und das Ergebnis wird eine Anwendung sein, die mühsam zu warten und zu entwickeln sein wird.
Der Benutzer der Klasse Article
hat keine Ahnung, wo die Methode save()
den Artikel speichert. In
einer Datenbanktabelle? In welcher, der Produktions- oder der Testtabelle? Und wie kann sie geändert werden?
Der Benutzer muss sich ansehen, wie die Methode save()
implementiert ist, und findet die Verwendung der Methode
DB::insert()
. Er muss also weiter suchen, um herauszufinden, wie diese Methode eine Datenbankverbindung herstellt.
Und versteckte Abhängigkeiten können eine ziemlich lange Kette bilden.
In sauberem und gut durchdachtem Code gibt es niemals versteckte Abhängigkeiten, Laravel-Fassaden oder statische Variablen. In sauberem und gut durchdachtem 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,
]);
}
}
Ein noch praktischerer Ansatz 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; es sollte eine reine Datenkomponente darstellen, und ein separates Repository sollte
sich um das Speichern kümmern. Das macht Sinn. Aber das würde weit über den Rahmen dieses Themas hinausgehen, das sich mit der
Injektion von Abhängigkeiten befasst, und den Versuch, einfache Beispiele zu liefern.
Wenn Sie eine Klasse schreiben, die zum Beispiel eine Datenbank für ihren Betrieb benötigt, erfinden Sie nicht, woher Sie diese bekommen, sondern lassen Sie sie übergeben. Entweder als Parameter des Konstruktors oder einer anderen Methode. Geben Sie Abhängigkeiten zu. Geben Sie sie in der API Ihrer Klasse an. Sie werden verständlichen und vorhersehbaren Code erhalten.
Und was ist mit dieser Klasse, die Fehlermeldungen protokolliert?
class Logger
{
public function log(string $message)
{
$file = LOG_DIR . '/log.txt';
file_put_contents($file, $message . "\n", FILE_APPEND);
}
}
Was meinen Sie, haben wir die Regel Nr. 1: Lass es dir übergeben: Es wird an Sie weitergegeben?
Wir haben es nicht getan.
Die Schlüsselinformation, d.h. das Verzeichnis mit der Protokolldatei, wird von der Klasse selbst aus der Konstante erhalten.
Sehen Sie sich das Beispiel für die Verwendung an:
$logger = new Logger;
$logger->log('The temperature is 23 °C');
$logger->log('The temperature is 10 °C');
Können Sie, ohne die Implementierung zu kennen, die Frage beantworten, wo die Nachrichten geschrieben werden? Würden Sie
vermuten, dass das Vorhandensein der Konstante LOG_DIR
für das Funktionieren des Programms notwendig ist? Und
könnten Sie eine zweite Instanz erstellen, die an einen anderen Ort schreibt? Sicherlich nicht.
Lassen Sie uns die Klasse korrigieren:
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, konfigurierbar und daher nützlicher.
$logger = new Logger('/path/to/log.txt');
$logger->log('The temperature is 15 °C');
Aber das ist mir egal!
„Wenn ich ein Artikel-Objekt erstelle und save() aufrufe, möchte ich mich nicht mit der Datenbank befassen; ich möchte nur, dass es in der Datenbank gespeichert wird, die ich in der Konfiguration eingestellt habe.“
„Wenn ich Logger verwende, möchte ich nur, dass die Nachricht geschrieben wird, und ich möchte mich nicht darum kümmern, wo. Es sollen die globalen Einstellungen verwendet werden.“
Dies sind berechtigte Einwände.
Betrachten wir als Beispiel eine Klasse, die Newsletter versendet und protokolliert, wie es gelaufen ist:
class NewsletterDistributor
{
public function distribute(): void
{
$logger = new Logger(/* ... */);
try {
$this->sendEmails();
$logger->log('Emails have been sent out');
} catch (Exception $e) {
$logger->log('An error occurred during the sending');
throw $e;
}
}
}
Bei der verbesserten Version Logger
, die nicht mehr die Konstante LOG_DIR
verwendet, muss der
Dateipfad im Konstruktor angegeben werden. Wie lässt sich das Problem lösen? Der Klasse NewsletterDistributor
ist
es egal, wohin die Nachrichten geschrieben werden; sie will sie einfach nur schreiben.
Die Lösung ist wieder Regel Nr. 1: Lass sie dir übergeben: Übergeben Sie alle Daten, die die Klasse benötigt.
Heißt das also, dass wir den Pfad zum Protokoll über den Konstruktor übergeben, den wir dann bei der Erstellung des
Logger
Objekts verwenden?
class NewsletterDistributor
{
public function __construct(
private string $file, // ⛔ NICHT AUF DIESE WEISE!
) {
}
public function distribute(): void
{
$logger = new Logger($this->file);
Nein, nicht auf diese Weise! Der Pfad gehört nicht zu den Daten, die die Klasse NewsletterDistributor
braucht,
sondern die Klasse Logger
braucht ihn. Verstehen Sie den Unterschied? Die Klasse NewsletterDistributor
braucht den Logger selbst. Das ist es also, was wir übergeben:
class NewsletterDistributor
{
public function __construct(
private Logger $logger, // ✅
) {
}
public function distribute(): void
{
try {
$this->sendEmails();
$this->logger->log('Emails have been sent out');
} catch (Exception $e) {
$this->logger->log('An error occurred during the sending');
throw $e;
}
}
}
Nun geht aus den Signaturen der Klasse NewsletterDistributor
hervor, dass auch die Protokollierung zu ihrer
Funktionalität gehört. Und die Aufgabe, den Logger gegen einen anderen auszutauschen, etwa zu Testzwecken, ist völlig trivial.
Wenn sich außerdem der Konstruktor der Klasse Logger
ändert, hat dies keine Auswirkungen auf unsere Klasse.
Regel Nr. 2: Nimm, was dir gehört
Lassen Sie sich nicht in die Irre führen und lassen Sie sich nicht die Abhängigkeiten von Ihren Abhängigen geben. Übergeben Sie nur Ihre eigenen Abhängigkeiten.
Dadurch wird der Code, der andere Objekte verwendet, völlig unabhängig von Änderungen in deren Konstruktoren. Seine API wird wahrheitsgetreuer sein. Und vor allem wird es trivial sein, diese Abhängigkeiten durch andere zu ersetzen.
Neues Familienmitglied
Das Entwicklungsteam beschloss, einen zweiten Logger zu erstellen, der in die Datenbank schreibt. Also erstellen wir eine
DatabaseLogger
Klasse. Wir haben also zwei Klassen, Logger
und DatabaseLogger
, eine
schreibt in eine Datei, die andere in eine Datenbank … kommt Ihnen die Namensgebung nicht seltsam vor? Wäre es nicht besser,
Logger
in FileLogger
umzubenennen? Eindeutig ja.
Aber lassen Sie uns das auf intelligente Weise tun. Wir erstellen eine Schnittstelle unter dem ursprünglichen Namen:
interface Logger
{
function log(string $message): void;
}
… die beide Logger implementieren werden:
class FileLogger implements Logger
// ...
class DatabaseLogger implements Logger
// ...
Daher muss im restlichen Code, in dem der Logger verwendet wird, nichts geändert werden. Zum Beispiel wird der Konstruktor der
Klasse NewsletterDistributor
immer noch damit zufrieden sein, Logger
als Parameter zu benötigen. Und es
bleibt uns überlassen, welche Instanz wir übergeben.
Deshalb fügen wir den Schnittstellennamen niemals das Suffix Interface
oder das Präfix I
hinzu. Sonst wäre es nicht möglich, den Code so schön zu entwickeln.
Houston, wir haben ein Problem
Während wir mit einer einzigen Instanz des Loggers, egal ob datei- oder datenbankbasiert, in der gesamten Anwendung auskommen
und sie einfach überall dort übergeben können, wo etwas protokolliert wird, verhält es sich bei der Klasse
Article
ganz anders. Wir erzeugen ihre Instanzen je nach Bedarf, sogar mehrfach. Wie geht man mit der
Datenbankabhängigkeit in ihrem Konstruktor um?
Ein Beispiel kann 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 liegt auf der Hand: Übergeben Sie das Datenbankobjekt an den EditController
Konstruktor
und verwenden Sie $article = new Article($this->db)
.
Genau wie im vorherigen Fall mit Logger
und dem Dateipfad ist dies nicht der richtige Ansatz. Die Datenbank ist
keine Abhängigkeit von EditController
, sondern von Article
. Die Übergabe der Datenbank verstößt
gegen Regel #2: Nimm, was dir gehört. Wenn sich der Konstruktor der Klasse
Article
ändert (ein neuer Parameter wird hinzugefügt), müssen Sie den Code überall dort ändern, wo Instanzen
erzeugt werden. Ufff.
Houston, was schlagen Sie vor?
Regel Nr. 3: Überlassen Sie die Abwicklung der Fabrik
Durch die Beseitigung versteckter Abhängigkeiten und die Übergabe aller Abhängigkeiten als Argumente haben wir mehr konfigurierbare und flexible Klassen erhalten. Und deshalb brauchen wir etwas anderes, um diese flexibleren Klassen für uns zu erstellen und zu konfigurieren. Wir werden es Fabriken nennen.
Die Faustregel lautet: Wenn eine Klasse Abhängigkeiten hat, überlassen Sie die Erstellung ihrer Instanzen der Fabrik.
Fabriken sind ein intelligenter Ersatz für den new
Operator in der Welt der Dependency Injection.
Nicht zu verwechseln mit dem Entwurfsmuster Fabrikmethode, das eine spezielle Art der Verwendung von Fabriken beschreibt und nichts mit diesem Thema zu tun hat.
Fabrik
Eine Fabrik ist eine Methode oder Klasse, die Objekte erstellt und konfiguriert. Wir werden die Klasse, die
Article
erzeugt, ArticleFactory
nennen, und sie könnte wie folgt aussehen:
class ArticleFactory
{
public function __construct(
private Nette\Database\Connection $db,
) {
}
public function create(): Article
{
return new Article($this->db);
}
}
Die Verwendung im Controller sieht folgendermaßen aus:
class EditController extends Controller
{
public function __construct(
private ArticleFactory $articleFactory,
) {
}
public function formSubmitted($data)
{
// die Fabrik ein Objekt erstellen lassen
$article = $this->articleFactory->create();
$article->title = $data->title;
$article->content = $data->content;
$article->save();
}
}
Wenn sich nun die Signatur des Konstruktors der Klasse Article
ändert, ist der einzige Teil des Codes, der darauf
reagieren muss, der ArticleFactory
selbst. Alle anderen Codes, die mit Article
Objekten arbeiten, wie
z.B. EditController
, sind davon nicht betroffen.
Sie werden sich vielleicht fragen, ob wir die Dinge tatsächlich besser gemacht haben. Die Menge des Codes hat zugenommen, und das Ganze sieht verdächtig kompliziert aus.
Keine Sorge, bald werden wir zum Nette-DI-Container kommen. Und der hat einige Tricks in petto, die das Erstellen von
Anwendungen mit Dependency Injection erheblich vereinfachen werden. Zum Beispiel müssen Sie anstelle der Klasse
ArticleFactory
nur eine einfache Schnittstelle schreiben:
interface ArticleFactory
{
function create(): Article;
}
Aber wir greifen uns selbst vor; bitte haben Sie noch etwas Geduld :-)
Zusammenfassung
Zu Beginn dieses Kapitels haben wir versprochen, Ihnen einen Prozess zur Entwicklung von sauberem Code zu zeigen. Alles, was es braucht, ist, dass die Klassen:
- die Abhängigkeiten zu übergeben, die sie benötigen
- umgekehrt nicht übergeben, was sie nicht direkt brauchen
- und dass Objekte mit Abhängigkeiten am besten in Fabriken erstellt werden
Auf den ersten Blick scheinen diese drei Regeln keine weitreichenden Konsequenzen zu haben, aber sie führen zu einer radikal anderen Sichtweise des Codeentwurfs. Ist es das wert? Entwickler, die alte Gewohnheiten aufgegeben und mit der konsequenten Nutzung von Dependency Injection begonnen haben, betrachten diesen Schritt als einen entscheidenden Moment in ihrem Berufsleben. Er hat ihnen die Welt der klaren und wartbaren Anwendungen eröffnet.
Was aber, wenn der Code nicht konsequent Dependency Injection verwendet? Was ist, wenn er sich auf statische Methoden oder Singletons stützt? Verursacht das Probleme? Ja, das tut es, und zwar ganz grundlegende.