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 erforderlich sind, um sauberen, verständlichen und wartbaren Code zu schreiben.
Wenn Sie diese Regeln lernen und befolgen, wird Nette Ihnen bei jedem Schritt zur Seite stehen. Es wird Routineaufgaben für Sie erledigen und es Ihnen so bequem wie möglich machen, damit Sie sich auf die eigentliche Logik konzentrieren können.
Die Prinzipien, die wir hier zeigen, sind ganz einfach. Sie müssen sich um nichts kümmern.
Erinnern Sie sich an Ihr erstes Programm?
Wir haben keine Ahnung, in welcher Sprache Sie es geschrieben haben, aber wenn es PHP war, würde es wahrscheinlich ungefähr 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 wir einer Funktion eine Eingabe übergeben und sie ein Ergebnis zurückliefert, ist ein vollkommen verständliches Konzept, das auch in anderen Bereichen, wie der Mathematik, verwendet wird.
Eine Funktion hat eine 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; über die interne Implementierung brauchen wir normalerweise nichts zu wissen.
Stellen Sie sich nun vor, dass die Signatur einer Funktion wie folgt aussieht:
function summe(float $x): float
Eine Addition mit einem Parameter? Das ist seltsam… Wie wäre es hiermit?
function summe(): float
Das ist wirklich seltsam, nicht wahr? Was glaubst du, wie die Funktion verwendet wird?
echo summe(); // was wird gedruckt?
Wenn wir uns einen solchen Code ansehen, sind wir verwirrt. Nicht nur ein Anfänger würde ihn nicht verstehen, 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 Addierer nehmen? Wahrscheinlich würde sie sie sich irgendwie selbst beschaffen, etwa 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 Design, das uns gerade gezeigt wurde, ist die Essenz vieler negativer Merkmale:
- die Funktionssignatur gibt vor, dass sie keine Summanden braucht, was uns verwirrt
- wir haben keine Ahnung, wie wir die Funktion mit zwei anderen Zahlen rechnen lassen können
- wir mussten in den Code schauen, um zu sehen, woher die Summanden kommen
- wir haben versteckte Bindungen entdeckt
- um alles zu verstehen, müssen wir auch diese Bindungen untersuchen
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 Mechanismen zu erfinden, die ihnen helfen, irgendwie selbst an die Daten zu kommen, übergeben Sie einfach die Parameter. So sparen Sie sich die Zeit, die Sie brauchen, um sich versteckte Wege auszudenken, die Ihren Code definitiv nicht verbessern.
Wenn Sie diese Regel immer und überall befolgen, sind Sie auf dem Weg zu Code ohne versteckte Bindungen. Auf dem Weg zu 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 fachmännisch dependency injection genannt. Und die Daten werden Abhängigkeiten genannt. Aber es ist eine einfache Parameterübergabe, nichts weiter.
Bitte verwechseln Sie nicht die Dependency Injection, die ein Entwurfsmuster ist, mit dem “Dependency Injection Container”, der ein Werkzeug ist, etwas völlig anderes. Wir werden später über Container sprechen.
Von Funktionen zu Klassen
Und was haben Klassen damit zu tun? Eine Klasse ist ein komplexeres Gebilde als eine einfache Funktion, aber auch hier gilt Regel Nr. 1. 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 die Verwendung anderer 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 Beispielen aus der realen Welt.
Nehmen wir eine Klasse Article
, die einen Blog-Artikel 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 wäre ein Kinderspiel, wenn es nicht einen Haken gäbe: Woher soll
Article
die Datenbankverbindung, d.h. das Objekt der Klasse Nette\Database\Connection
bekommen?
Es scheint, dass wir eine Menge Möglichkeiten haben. Es kann sie von irgendwoher aus einer statischen Variable beziehen. Oder sie von einer Klasse erben, die die Datenbankverbindung bereitstellt. Oder die Vorteile eines Singletons nutzen. Oder die sogenannten Fassaden, 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: Lass es dir übergeben: Alle Abhängigkeiten, die die Klasse benötigt, müssen an sie weitergegeben werden. Denn wenn wir das nicht tun und gegen die Regel verstoßen, haben wir den Weg zu schmutzigem Code voller versteckter Bindungen und Unverständlichkeit eingeschlagen, und das Ergebnis wird eine Anwendung sein, die nur schwer zu warten und zu entwickeln ist.
Der Benutzer der Klasse Article
hat keine Ahnung, wo die Methode save()
den Artikel speichert. In
einer Datenbanktabelle? In welcher, der Produktions- oder der Entwicklungstabelle? Und wie kann dies geändert werden?
Der Benutzer muss sich ansehen, wie die Methode save()
implementiert ist, um die Verwendung der Methode
DB::insert()
zu finden. Er muss also weiter suchen, um herauszufinden, wie diese Methode eine Datenbankverbindung
herstellt. Und versteckte Bindungen können eine ziemlich lange Kette bilden.
Versteckte Bindungen, Laravel-Fassaden oder statische Variablen sind in sauberem, gut durchdachtem Code nie vorhanden. 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,
]);
}
}
Noch praktischer ist es, wie wir gleich sehen werden, einen Konstruktor zu verwenden:
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
save()
-Methode haben sollte, sondern eine reine Datenkomponente sein sollte, und dass ein separates Repository sich
um die Speicherung kümmern sollte. Das macht auch Sinn. Aber das würde weit über das Thema hinausgehen, bei dem es um
Dependency Injection geht, und wir würden versuchen, einfache Beispiele zu geben.
Wenn Sie eine Klasse schreiben, die zum Beispiel eine Datenbank benötigt, um zu funktionieren, sollten Sie nicht herausfinden, woher Sie sie bekommen, sondern sie an sich selbst übergeben. Vielleicht als Parameter in einem Konstruktor oder einer anderen Methode. Deklarieren Sie Abhängigkeiten. Legen Sie sie in der API Ihrer Klasse offen. So erhalten Sie verständlichen und vorhersehbaren Code.
Wie wäre es 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, das Verzeichnis der Protokolldatei, wird von der Klasse aus der Konstante erhalten.
Siehe das Verwendungsbeispiel:
$logger = new Logger;
$logger->log('The temperature is 23 °C');
$logger->log('The temperature is 10 °C');
Könnten 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 notwendig ist, damit es funktioniert? Und wären Sie in der Lage, eine zweite Instanz zu 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 übersichtlicher, besser 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 Artikelobjekt 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 korrekte Kommentare.
Nehmen wir als Beispiel eine Klasse, die Newsletter verschickt und protokolliert, wie das 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;
}
}
}
Die verbesserte Logger
, die nicht mehr die Konstante LOG_DIR
verwendet, erfordert einen Dateipfad im
Konstruktor. Wie lässt sich das Problem lösen? Der Klasse NewsletterDistributor
ist es egal, wohin die Nachrichten
geschrieben werden, sie will sie nur schreiben.
Die Lösung ist wieder Regel Nr. 1: Lass es dir übergeben: Gib der Klasse alle Daten, die sie braucht.
Wir übergeben also den Pfad zum Protokoll an den Konstruktor, mit dem wir dann das Objekt Logger
erstellen ?
class NewsletterDistributor
{
public function __construct(
private string $file, // ⛔ NICHT AUF DIESE WEISE!
) {
}
public function distribute(): void
{
$logger = new Logger($this->file);
So nicht! Denn der Pfad gehört nicht zu den Daten, die die Klasse NewsletterDistributor
braucht; sie
braucht Logger
. Die Klasse braucht den Logger selbst. Und den werden wir weitergeben:
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;
}
}
}
Aus den Signaturen der Klasse NewsletterDistributor
geht hervor, dass die Protokollierung Teil ihrer
Funktionalität ist. Und die Aufgabe, den Logger durch einen anderen zu ersetzen, vielleicht zu Testzwecken, ist recht trivial.
Wenn der Konstruktor der Klasse Logger
geändert wird, 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 Parameter Ihrer Abhängigkeiten übergeben. Geben Sie die Abhängigkeiten direkt weiter.
Dadurch wird Code, der andere Objekte verwendet, völlig unabhängig von Änderungen an deren Konstruktoren. Seine API wird wahrheitsgetreuer sein. Und das Wichtigste: Es wird trivial sein, diese Abhängigkeiten gegen andere auszutauschen.
Ein neues Mitglied der Familie
Das Entwicklungsteam beschloss, einen zweiten Logger zu erstellen, der in die Datenbank schreibt. Wir erstellen also eine
Klasse DatabaseLogger
. Wir haben also zwei Klassen, Logger
und DatabaseLogger
, die eine
schreibt in eine Datei, die andere in eine Datenbank … finden Sie nicht auch, dass dieser Name etwas seltsam ist? Wäre es nicht
besser, Logger
in FileLogger
umzubenennen? Sicher wäre es das.
Aber lassen Sie uns das klug anstellen. Wir werden eine Schnittstelle unter dem ursprünglichen Namen erstellen:
interface Logger
{
function log(string $message): void;
}
…die beide Logger implementieren werden:
class FileLogger implements Logger
// ...
class DatabaseLogger implements Logger
// ...
Auf diese Weise 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 an ihn übergeben.
Das ist der Grund, warum wir Schnittstellennamen niemals das Suffix Interface
oder das Präfix I
geben. Andernfalls wäre es unmöglich, Code so schön zu entwickeln.
Houston, wir haben ein Problem
Während wir uns in der gesamten Anwendung mit einer einzigen Instanz eines Loggers, ob Datei oder Datenbank, begnügen und
diese einfach überall dort übergeben können, wo etwas protokolliert wird, sieht es im Fall der Klasse Article
ganz
anders aus. Wir erstellen nämlich Instanzen davon nach Bedarf, möglicherweise mehrfach. Wie geht man mit der Datenbankanbindung
in ihrem Konstruktor um?
Als Beispiel können wir einen Controller verwenden, 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 bietet sich direkt an: Lassen Sie das Datenbankobjekt vom Konstruktor an EditController
übergeben und verwenden Sie $article = new Article($this->db)
.
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 also gegen
Regel #2: Nimm, was dir gehört. Wenn der Konstruktor der Klasse Article
geändert wird (ein neuer Parameter wird hinzugefügt), muss der Code an allen Stellen, an denen Instanzen erstellt werden,
ebenfalls geändert werden. Ufff.
Houston, was schlägst du vor?
Regel Nr. 3: Überlassen Sie die Abwicklung der Fabrik
Wenn wir die versteckten Bindungen entfernen und alle Abhängigkeiten als Argumente übergeben, erhalten wir flexiblere und besser konfigurierbare Klassen. Und deshalb brauchen wir etwas anderes, um diese flexibleren Klassen zu erstellen und zu konfigurieren. Nennen wir es Fabriken.
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.
Fabrik
Eine Fabrik ist eine Methode oder Klasse, die Objekte erzeugt und konfiguriert. Wir nennen die produzierende Klasse
Article
ArticleFactory
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);
}
}
Ihre Verwendung im Controller würde folgendermaßen aussehen:
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 die Signatur des Konstruktors der Klasse Article
ändert, ist der einzige Teil des Codes, der darauf
reagieren muss, die ArticleFactory
factory selbst. Jeder andere Code, der mit Article
Objekten arbeitet,
wie z. B. EditController
, ist davon nicht betroffen.
Vielleicht tippen Sie sich jetzt an die Stirn und fragen sich, ob wir uns überhaupt geholfen haben. Die Menge des Codes ist gewachsen und das Ganze sieht langsam verdächtig kompliziert aus.
Keine Sorge, wir werden bald zum Nette-DI-Container kommen. Und der hat eine Reihe von Trümpfen im Ärmel, die das Erstellen
von Anwendungen mit Dependency Injection extrem vereinfachen. Zum Beispiel genügt es, statt der Klasse
ArticleFactory
eine einfache Schnittstelle zu
schreiben:
interface ArticleFactory
{
function create(): Article;
}
Aber wir kommen der Sache zuvor, warten Sie ab :-)
Zusammenfassung
Zu Beginn dieses Kapitels haben wir versprochen, Ihnen einen Weg zu zeigen, wie Sie sauberen Code entwerfen können. Geben Sie einfach den Klassen
- die Abhängigkeiten, die sie brauchen
- und nicht das, was sie nicht direkt brauchen
- und dass Objekte mit Abhängigkeiten am besten in Fabriken erstelltwerden
Es mag auf den ersten Blick nicht so aussehen, aber diese drei Regeln haben weitreichende Auswirkungen. Sie führen zu einer radikal anderen Sichtweise des Codeentwurfs. Ist es das wert? Programmierer, die alte Gewohnheiten über Bord geworfen haben und mit der konsequenten Anwendung von Dependency Injection begonnen haben, betrachten dies als einen Schlüsselmoment in ihrem Berufsleben. Es eröffnete ihnen eine Welt klarer und nachhaltiger Anwendungen.
Aber was ist, wenn der Code nicht konsequent mit Dependency Injection arbeitet? Was ist, wenn er auf statischen Methoden oder Singletons aufbaut? Bringt das irgendwelche Probleme mit sich? Das tut es, und es ist sehr bedeutsam.