Was ist Dependency Injection?
Dieses Kapitel führt Sie in die grundlegenden Programmierpraktiken ein, die Sie beim Schreiben aller Anwendungen befolgen sollten. Dies sind die Grundlagen für das Schreiben von sauberem, verständlichem und wartbarem Code.
Wenn Sie sich diese Regeln aneignen und befolgen, wird Nette Sie bei jedem Schritt unterstützen. Es wird Routineaufgaben für Sie erledigen und Ihnen maximalen Komfort bieten, damit Sie sich auf die eigentliche Logik konzentrieren können.
Die Prinzipien, die wir hier vorstellen werden, sind dabei recht einfach. Sie müssen sich keine Sorgen machen.
Erinnern Sie sich an Ihr erstes Programm?
Wir wissen nicht, in welcher Sprache Sie es geschrieben haben, aber wenn es PHP gewesen wäre, hätte es wahrscheinlich so ausgesehen:
function soucet(float $a, float $b): float
{
return $a + $b;
}
echo soucet(23, 1); // gibt 24 aus
Ein paar triviale Codezeilen, aber sie enthalten so viele Schlüsselkonzepte. Dass es Variablen gibt. Dass Code in kleinere Einheiten unterteilt wird, wie zum Beispiel Funktionen. Dass wir ihnen Eingabeargumente übergeben und sie Ergebnisse zurückgeben. Es fehlen nur noch Bedingungen und Schleifen.
Dass wir einer Funktion Eingabedaten übergeben und sie ein Ergebnis zurückgibt, ist ein perfekt verständliches Konzept, das auch in anderen Bereichen verwendet wird, wie zum Beispiel in der Mathematik.
Eine Funktion hat ihre Signatur, die aus ihrem Namen, einer Übersicht der Parameter und ihrer Typen und schließlich dem Typ des Rückgabewerts besteht. Als Benutzer interessiert uns die Signatur, über die interne Implementierung müssen wir normalerweise nichts wissen.
Stellen Sie sich nun vor, die Funktionssignatur sähe so aus:
function soucet(float $x): float
Eine Summe mit einem Parameter? Das ist seltsam… Und was ist hiermit?
function soucet(): float
Das ist jetzt wirklich sehr seltsam, oder? Wie wird die Funktion wohl verwendet?
echo soucet(); // was gibt das wohl aus?
Beim Anblick eines solchen Codes wären wir verwirrt. Nicht nur ein Anfänger würde ihn nicht verstehen, auch ein erfahrener Programmierer versteht solchen Code nicht.
Überlegen Sie, wie eine solche Funktion intern aussehen würde? Woher nimmt sie die Summanden? Wahrscheinlich würde sie sie sich irgendwie selbst beschaffen, vielleicht so:
function soucet(): float
{
$a = Input::get('a');
$b = Input::get('b');
return $a + $b;
}
Im Funktionskörper haben wir versteckte Abhängigkeiten zu anderen globalen Funktionen oder statischen Methoden entdeckt. Um herauszufinden, woher die Summanden tatsächlich stammen, müssen wir weiter suchen.
So nicht!
Der Entwurf, den wir gerade vorgestellt haben, ist die Essenz vieler negativer Eigenschaften:
- Die Funktionssignatur tat so, als ob sie keine Summanden bräuchte, was uns verwirrte
- Wir wissen überhaupt nicht, wie wir die Funktion dazu bringen können, zwei andere Zahlen zu addieren
- Wir mussten uns den Code ansehen, um herauszufinden, woher sie die Summanden nimmt
- Wir haben versteckte Abhängigkeiten entdeckt
- Für ein vollständiges Verständnis müssen auch diese Abhängigkeiten untersucht werden
Und ist es überhaupt die Aufgabe einer Additionsfunktion, sich Eingaben zu beschaffen? Natürlich nicht. Ihre Verantwortung liegt ausschließlich in der Addition selbst.
Solchen Code wollen wir nicht sehen und schon gar nicht schreiben. Die Korrektur ist dabei einfach: Zurück zu den Grundlagen und einfach Parameter verwenden:
function soucet(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 ihnen übergeben werden.
Anstatt versteckte Wege zu erfinden, über die sie irgendwie selbst an die Daten gelangen könnten, übergeben Sie einfach die Parameter. Sie sparen sich die Zeit, die für das Ausdenken versteckter Pfade benötigt wird, die Ihren Code definitiv nicht verbessern werden.
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 nach ihm liest. 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 als Dependency Injection bezeichnet. Und diese Daten werden Abhängigkeiten genannt. Dabei handelt es sich um die einfache Übergabe von Parametern, nichts weiter.
Bitte verwechseln Sie Dependency Injection, ein Entwurfsmuster, nicht mit einem „Dependency Injection Container“, einem Werkzeug, also etwas völlig anderem. Container werden wir später behandeln.
Von Funktionen zu Klassen
Und wie hängt das mit Klassen zusammen? Eine Klasse ist eine komplexere Einheit als eine einfache Funktion, aber Regel Nr. 1 gilt hier uneingeschränkt. Es gibt nur mehr Möglichkeiten, Argumente zu übergeben. Zum Beispiel ganz ähnlich wie im Fall einer Funktion:
class Matematika
{
public function soucet(float $a, float $b): float
{
return $a + $b;
}
}
$math = new Matematika;
echo $math->soucet(23, 1); // 24
Oder über andere Methoden oder direkt über den Konstruktor:
class Soucet
{
public function __construct(
private float $a,
private float $b,
) {
}
public function spocti(): float
{
return $this->a + $this->b;
}
}
$soucet = new Soucet(23, 1);
echo $soucet->spocti(); // 24
Beide Beispiele stehen vollständig im Einklang mit Dependency Injection.
Reale Beispiele
In der realen Welt werden Sie keine Klassen zum Addieren von Zahlen schreiben. Gehen wir zu Beispielen aus der Praxis ü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
{
// wir speichern 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, gäbe es nicht einen Haken: Woher nimmt
Article
die Datenbankverbindung, d.h. das Objekt der Klasse Nette\Database\Connection
?
Es scheint, wir haben viele Möglichkeiten. Sie könnte sie irgendwoher aus einer statischen Variablen nehmen. Oder von einer Klasse erben, die die Datenbankverbindung bereitstellt. Oder das sogenannte Singleton verwenden. Oder sogenannte Facades, wie sie 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],
);
}
}
Großartig, wir haben das Problem gelöst.
Oder nicht?
Erinnern wir uns an Regel Nr. 1: Lass es dir übergeben: Alle Abhängigkeiten, die eine Klasse benötigt, müssen ihr übergeben werden. Denn wenn wir die Regel verletzen, haben wir den Weg zu schmutzigem Code voller versteckter Abhängigkeiten und Unverständlichkeit eingeschlagen, und das Ergebnis wird eine Anwendung sein, deren Wartung und Entwicklung mühsam sein wird.
Der Benutzer der Klasse Article
weiß nicht, wohin die Methode save()
den Artikel speichert. In eine
Datenbanktabelle? In welche, die Produktiv- oder die Testdatenbank? Und wie kann man das ändern?
Der Benutzer muss sich ansehen, wie die Methode save()
implementiert ist, und findet die Verwendung der Methode
DB::insert()
. Also muss er weiter suchen, wie diese Methode die Datenbankverbindung beschafft. 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, wie wir später sehen werden, ist es über den Konstruktor:
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 die Speicherung von einem separaten
Repository übernommen werden sollte. Das macht Sinn. Aber damit würden wir weit über das Thema Dependency Injection hinausgehen
und das Bemühen, einfache Beispiele zu geben, sprengen.
Wenn Sie eine Klasse schreiben, die für ihre Tätigkeit z. B. eine Datenbank benötigt, überlegen Sie nicht, woher Sie sie bekommen, sondern lassen Sie sie sich übergeben. Zum Beispiel als Parameter des Konstruktors oder einer anderen Methode. Geben Sie Abhängigkeiten zu. Geben Sie sie in der API Ihrer Klasse zu. Sie erhalten verständlichen und vorhersagbaren Code.
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 Regel Nr. 1: Lass es dir übergeben eingehalten?
Nein, haben wir nicht.
Die Schlüsselinformation, nämlich das Verzeichnis mit der Logdatei, beschafft sich die Klasse selbst aus einer Konstante.
Sehen Sie sich das Anwendungsbeispiel an:
$logger = new Logger;
$logger->log('Temperatur ist 23 °C');
$logger->log('Temperatur ist 10 °C');
Ohne Kenntnis der Implementierung, könnten Sie die Frage beantworten, wohin die Nachrichten geschrieben werden? Wäre Ihnen
eingefallen, dass für die Funktion die Existenz der Konstante LOG_DIR
erforderlich ist? Und könnten Sie eine zweite
Instanz erstellen, die woanders hinschreibt? Sicher 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, konfigurierbarer und daher nützlicher.
$logger = new Logger('/pfad/zum/log.txt');
$logger->log('Temperatur ist 15 °C');
Aber das interessiert mich nicht!
„Wenn ich ein Article-Objekt erstelle und save() aufrufe, will ich mich nicht um die Datenbank kümmern, ich will einfach, dass es in die Datenbank gespeichert wird, die ich in der Konfiguration eingestellt habe.“
„Wenn ich Logger verwende, will ich einfach, dass die Nachricht geschrieben wird, und ich will mich nicht darum kümmern, wohin. Es soll die globale Einstellung verwendet werden.“
Das sind berechtigte Einwände.
Als Beispiel zeigen wir 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('E-Mails wurden versendet');
} catch (Exception $e) {
$logger->log('Fehler beim Versenden aufgetreten');
throw $e;
}
}
}
Der verbesserte Logger
, der die Konstante LOG_DIR
nicht mehr verwendet, erfordert im Konstruktor die
Angabe des Dateipfads. Wie löst man das? Die Klasse NewsletterDistributor
interessiert sich überhaupt nicht dafür,
wohin die Nachrichten geschrieben werden, sie will sie nur schreiben.
Die Lösung ist wieder Regel Nr. 1: Lass es dir übergeben: Alle Daten, die die Klasse benötigt, übergeben wir ihr.
Bedeutet das also, dass wir uns den Pfad zum Log über den Konstruktor übergeben, den wir dann beim Erstellen des
Logger
-Objekts verwenden?
class NewsletterDistributor
{
public function __construct(
private string $file, // ⛔ SO NICHT!
) {
}
public function distribute(): void
{
$logger = new Logger($this->file);
So nicht! Der Pfad gehört nämlich nicht zu den Daten, die die Klasse NewsletterDistributor
benötigt;
diese benötigt der Logger
. Erkennen Sie den Unterschied? Die Klasse NewsletterDistributor
benötigt den
Logger als solchen. Also übergeben wir uns diesen:
class NewsletterDistributor
{
public function __construct(
private Logger $logger, // ✅
) {
}
public function distribute(): void
{
try {
$this->sendEmails();
$this->logger->log('E-Mails wurden versendet');
} catch (Exception $e) {
$this->logger->log('Fehler beim Versenden aufgetreten');
throw $e;
}
}
}
Nun ist aus den Signaturen der Klasse NewsletterDistributor
klar, dass auch Logging Teil ihrer Funktionalität
ist. Und die Aufgabe, den Logger gegen einen anderen auszutauschen, z. B. zum Testen, ist völlig trivial. Außerdem: Wenn sich
der Konstruktor der Klasse Logger
ändern würde, hätte dies keinerlei Auswirkungen auf unsere Klasse.
Regel Nr. 2: Nimm, was deins ist
Lassen Sie sich nicht täuschen und lassen Sie sich nicht die Abhängigkeiten Ihrer Abhängigkeiten übergeben. Lassen Sie sich nur Ihre eigenen Abhängigkeiten übergeben.
Dank dessen wird der Code, der andere Objekte verwendet, völlig unabhängig von Änderungen an deren Konstruktoren. Seine API wird wahrheitsgetreuer sein. Und vor allem wird es trivial sein, diese Abhängigkeiten gegen andere auszutauschen.
Neues Familienmitglied
Im Entwicklungsteam wurde beschlossen, einen zweiten Logger zu erstellen, der in die Datenbank schreibt. Wir erstellen also die
Klasse DatabaseLogger
. Wir haben also zwei Klassen, Logger
und DatabaseLogger
, eine
schreibt in eine Datei, die andere in die Datenbank … scheint Ihnen an dieser Benennung nicht etwas seltsam? Wäre es nicht
besser, Logger
in FileLogger
umzubenennen? Sicherlich ja.
Aber wir machen es clever. Unter dem ursprünglichen Namen erstellen wir eine Schnittstelle:
interface Logger
{
function log(string $message): void;
}
… die beide Logger implementieren werden:
class FileLogger implements Logger
// ...
class DatabaseLogger implements Logger
// ...
Und dank dessen muss im Rest des Codes, wo der Logger verwendet wird, nichts geändert werden. Zum Beispiel wird der
Konstruktor der Klasse NewsletterDistributor
weiterhin damit zufrieden sein, dass er als Parameter
Logger
benötigt. Und es liegt nur an uns, welche Instanz wir ihm übergeben.
Deshalb geben wir Schnittstellennamen niemals das Suffix Interface
oder das Präfix I
. Sonst
wäre es nicht möglich, den Code so schön weiterzuentwickeln.
Houston, wir haben ein Problem
Während wir in der gesamten Anwendung mit einer einzigen Instanz des Loggers auskommen können, sei es datei- oder
datenbankbasiert, und ihn einfach überall dorthin übergeben, wo etwas protokolliert wird, ist es bei der Klasse
Article
ganz anders. Ihre Instanzen erstellen wir nach Bedarf, gerne auch mehrmals. Wie gehen wir mit der
Abhängigkeit zur Datenbank in ihrem Konstruktor um?
Als Beispiel kann ein Controller dienen, 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: Wir lassen uns das Datenbankobjekt über den Konstruktor in
EditController
übergeben und verwenden $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
. Sich die Datenbank übergeben zu lassen,
verstößt also gegen Regel Nr. 2: Nimm, was deins ist. Wenn sich der Konstruktor
der Klasse Article
ändert (ein neuer Parameter kommt hinzu), muss auch der Code an allen Stellen angepasst werden,
an denen Instanzen erstellt werden. Uff.
Houston, was schlagen Sie vor?
Regel Nr. 3: Überlasse es der Fabrik
Dadurch, dass wir versteckte Abhängigkeiten beseitigt und alle Abhängigkeiten als Argumente übergeben haben, haben wir konfigurierbarere und flexiblere Klassen erhalten. Und daher brauchen wir noch etwas anderes, das uns diese flexibleren Klassen erstellt und konfiguriert. Wir nennen es Fabriken.
Die Regel lautet: Wenn eine Klasse Abhängigkeiten hat, überlasse die Erstellung ihrer Instanzen einer Fabrik.
Fabriken sind der clevere Ersatz für den new
-Operator in der Welt der Dependency Injection.
Bitte verwechseln Sie dies nicht mit dem Entwurfsmuster Factory Method, das eine spezifische Art der Verwendung von Fabriken beschreibt und mit diesem Thema nichts zu tun hat.
Fabrik
Eine Fabrik ist eine Methode oder Klasse, die Objekte herstellt und konfiguriert. Die Klasse, die Article
herstellt, nennen wir ArticleFactory
und sie könnte beispielsweise 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 wird wie folgt sein:
class EditController extends Controller
{
public function __construct(
private ArticleFactory $articleFactory,
) {
}
public function formSubmitted($data)
{
// lassen wir die Fabrik das Objekt erstellen
$article = $this->articleFactory->create();
$article->title = $data->title;
$article->content = $data->content;
$article->save();
}
}
Wenn sich zu diesem Zeitpunkt die Signatur des Konstruktors der Klasse Article
ändert, ist der einzige Teil des
Codes, der darauf reagieren muss, die Fabrik ArticleFactory
selbst. Aller anderer Code, der mit
Article
-Objekten arbeitet, wie zum Beispiel EditController
, bleibt davon unberührt.
Vielleicht klopfen Sie sich jetzt an die Stirn, ob wir uns überhaupt geholfen haben. Die Menge an Code ist gewachsen und das Ganze beginnt verdächtig kompliziert auszusehen.
Keine Sorge, wir kommen gleich zum Nette DI Container. Und der hat eine Reihe von Assen im Ärmel, die den Bau von Anwendungen,
die Dependency Injection verwenden, enorm vereinfachen. So wird beispielsweise anstelle der Klasse ArticleFactory
nur
das Schreiben einer reinen Schnittstelle ausreichen:
interface ArticleFactory
{
function create(): Article;
}
Aber das greifen wir vor, bleiben Sie noch dran :-)
Zusammenfassung
Am Anfang dieses Kapitels haben wir versprochen, Ihnen einen Ansatz zu zeigen, wie man sauberen Code entwirft. Es genügt, den Klassen
- die Abhängigkeiten zu übergeben, die sie benötigen
- und umgekehrt nicht das zu übergeben, was sie nicht direkt benötigen
- und dass Objekte mit Abhängigkeiten am besten in Fabriken hergestellt werden
Es mag auf den ersten Blick nicht so erscheinen, aber diese drei Regeln haben weitreichende Konsequenzen. Sie führen zu einer radikal anderen Sichtweise auf das Code-Design. Lohnt es sich? Programmierer, die alte Gewohnheiten aufgegeben haben und konsequent Dependency Injection verwenden, betrachten diesen Schritt als einen entscheidenden Moment in ihrer beruflichen Laufbahn. Es öffnete sich ihnen die Welt übersichtlicher und wartbarer Anwendungen.
Was aber, wenn der Code Dependency Injection nicht konsequent verwendet? Was, wenn er auf statischen Methoden oder Singletons basiert? Bringt das irgendwelche Probleme mit sich? Ja, und zwar sehr grundlegende.