Häufig gestellte Fragen zu DI (FAQ)

Ist DI ein anderer Name für IoC?

Inversion of Control (IoC) ist ein Prinzip, das den Kontrollfluss in einem Programm beschreibt: Ruft Ihr Code fremden Code auf, oder ruft fremder Code (etwa ein Framework) Ihren Code auf? IoC ist ein weiter Begriff, der Events, das sogenannte Hollywood-Prinzip und weitere Aspekte umfasst. Zu diesem Begriff gehören auch die Factories, die wir in Regel Nr. 3: Überlassen Sie es der Factory besprochen haben und die eine Umkehrung des Operators new darstellen.

Dependency Injection (DI) dreht sich darum, wie Objekte ihre Abhängigkeiten bekommen (also die anderen Objekte, mit denen sie arbeiten müssen). Es ist ein Entwurfsmuster, das dafür wirbt, Objekten die Abhängigkeiten ausdrücklich zu übergeben, statt sie von den Objekten erzeugen oder suchen zu lassen.

DI lässt sich deshalb als eine besondere Form von IoC verstehen. Nicht jede Form von IoC führt allerdings zu sauberem Code. Antipatterns sind zum Beispiel Techniken, die sich auf globalen Zustand oder auf das Muster Service Locator stützen.

Was ist ein Service Locator?

Es ist ein alternativer Ansatz zur Dependency Injection. Dabei gibt es ein zentrales Objekt (den Locator), bei dem alle verfügbaren Services (Abhängigkeiten) registriert sind. Braucht ein Objekt eine Abhängigkeit, fordert es sie beim Service Locator an.

Gegenüber DI fehlt ihm allerdings die Transparenz. Die Abhängigkeiten stecken versteckt im Code des Objekts (in den Aufrufen des Locators), statt in seiner API (im Konstruktor oder in Methoden) ausdrücklich zu stehen, sodass man den Code lesen muss, um die Zusammenhänge zu verstehen. Auch das Testen ist aufwendiger, denn Sie können beim Erzeugen eines Objekts nicht einfach Mock-Abhängigkeiten übergeben, sondern müssen häufig den Service Locator selbst manipulieren. Außerdem führt der Service Locator eine unnötige Abhängigkeit ein: Die Objekte werden an den Locator gekoppelt, anders als bei DI, wo die Objekte im Idealfall nichts vom Container wissen.

Wann verwendet man DI besser nicht?

Es sind keine nennenswerten Nachteile bekannt, wenn man das Entwurfsmuster Dependency Injection richtig einsetzt. Im Gegenteil führt es zu zahlreichen Komplikationen, Abhängigkeiten aus global zugänglichen Orten zu holen (etwa aus statischen Properties oder Singletons), und ebenso, einen Service Locator zu verwenden. DI zu verwenden ist deshalb im Allgemeinen immer ratsam. Das ist kein Dogma; es hat sich schlicht keine bessere Alternative durchgesetzt, um Abhängigkeiten auf saubere Weise zu verwalten.

Es gibt jedoch bestimmte, eng begrenzte Situationen, in denen der globale Zugriff auf Objekte vertretbar sein kann. Zum Beispiel beim Debuggen, wenn Sie den Wert einer Variablen ausgeben, die Laufzeit messen oder an einer bestimmten Stelle eine Meldung protokollieren müssen. In diesen Fällen, in denen es um vorübergehende Eingriffe geht, die später wieder aus dem Code verschwinden, kann ein global zugänglicher Dumper, Timer oder Logger legitim sein. Diese Werkzeuge sind nicht Teil des eigentlichen Entwurfs der Anwendung.

Hat DI Nachteile?

Bringt der Einsatz von Dependency Injection Nachteile mit sich, etwa mehr Schreibarbeit oder geringere Leistung? Was verlieren wir, wenn wir anfangen, Code nach den Prinzipien der DI zu schreiben?

DI selbst hat vernachlässigbaren Einfluss auf die Laufzeit oder den Speicherverbrauch. Die Leistung des DI-Containers kann eine Rolle spielen, aber Nette DI kompiliert den Container zu schlichtem PHP-Code, sodass beim Ausführen der Anwendung praktisch kein Mehraufwand entsteht.

Wenn Sie Code nach den Prinzipien der DI schreiben, müssen Sie oft Konstruktoren anlegen, die Abhängigkeiten entgegennehmen. Was früher mühsam wirkte, geht mit modernen IDEs und Fähigkeiten wie der Constructor Property Promotion von PHP 8 sehr schnell. Factories kann Nette DI oft automatisch erzeugen, was den Boilerplate-Code weiter verringert. Dafür entfällt das Schreiben von Singletons und statischen Accessors.

Insgesamt ist eine gut entworfene Anwendung mit DI üblicherweise weder deutlich kürzer noch deutlich länger als eine, die sich auf Singletons oder globalen Zugriff stützt. Der Code für das Erzeugen und Verdrahten von Abhängigkeiten wandert einfach aus den einzelnen Klassen an dafür vorgesehene Orte: in die Konfiguration des DI-Containers und in die Factories.

Wie schreibt man eine Altanwendung auf DI um?

Der Umstieg einer Altanwendung auf Dependency Injection kann ein anspruchsvoller Prozess sein, besonders bei großen und komplexen Anwendungen. Es ist wichtig, systematisch vorzugehen.

  • Beim Umstieg auf Dependency Injection ist es wichtig, dass alle Teammitglieder die verwendeten Prinzipien und Praktiken verstehen.
  • Analysieren Sie zuerst die bestehende Anwendung, um die zentralen Komponenten und ihre Abhängigkeiten zu erkennen. Erstellen Sie einen Plan, welche Teile in welcher Reihenfolge umgebaut werden.
  • Setzen Sie einen DI-Container um oder verwenden Sie besser eine bestehende Bibliothek wie Nette DI.
  • Bauen Sie Teile der Anwendung nach und nach so um, dass sie Dependency Injection verwenden. Das kann bedeuten, Konstruktoren oder Methoden so zu ändern, dass sie Abhängigkeiten als Parameter entgegennehmen.
  • Passen Sie den Code an den Stellen an, an denen Objekte erzeugt werden, sodass sie aus dem Container geholt oder über Factories des Containers erzeugt werden.

Denken Sie daran, dass der Umstieg auf Dependency Injection eine Investition in die Qualität des Codes und die langfristige Wartbarkeit der Anwendung ist. Diese Änderungen vorzunehmen mag anspruchsvoll sein, das Ergebnis sollte aber saubererer, modularerer und leicht testbarer Code sein, der für künftige Erweiterungen und Wartung bereit ist.

Warum ist Komposition der Vererbung vorzuziehen?

Für die Wiederverwendung von Code ist Komposition der Vererbung im Allgemeinen vorzuziehen, weil sie zu loserer Kopplung führt. Bei Komposition geraten Sie seltener in die Lage, dass eine Änderung an einer Basisklasse abhängige Unterklassen zerstört. Ein typisches Beispiel ist die Situation, die als Constructor Hell bekannt ist.

Lässt sich der Nette DI Container außerhalb von Nette verwenden?

Auf jeden Fall. Der Nette DI Container ist Teil von Nette, aber als eigenständige Bibliothek entworfen, die sich unabhängig von den übrigen Teilen des Frameworks verwenden lässt. Installieren Sie ihn einfach über Composer, legen Sie eine Konfigurationsdatei mit Ihren Services an und erzeugen Sie den DI-Container mit ein paar Zeilen PHP-Code. Und schon können Sie in Ihren Projekten von Dependency Injection profitieren.

Das Kapitel über den Nette DI Container beschreibt einen konkreten Anwendungsfall samt Codebeispielen.

Warum steht die Konfiguration in NEON-Dateien?

NEON ist eine einfache und gut lesbare Konfigurationssprache, die innerhalb von Nette entstanden ist, um Anwendungen, Services und ihre Abhängigkeiten einzurichten. Gegenüber JSON oder YAML bietet sie dafür deutlich intuitivere und flexiblere Möglichkeiten. In NEON lassen sich Definitionen von Services und Beziehungen natürlich beschreiben, die sich in JSON oder YAML nur schwer oder gar nicht so klar ausdrücken ließen.

Bremst das Parsen von NEON-Dateien die Anwendung aus?

NEON-Dateien werden zwar sehr schnell geparst, ihre Parsing-Geschwindigkeit ist in der Produktion aber weitgehend belanglos. Die Konfigurationsdateien werden nämlich nur einmal geparst, wenn die Anwendung zum ersten Mal läuft (oder wenn sie sich ändern). Nach dem Parsen wird der Code des DI-Containers erzeugt, gecacht (auf der Festplatte abgelegt), und bei jedem weiteren Request läuft dieser kompilierte PHP-Code, sodass kein weiteres Parsen nötig ist.

So funktioniert es in einer Produktionsumgebung. Während der Entwicklung werden die NEON-Dateien jedes Mal geparst, wenn sich ihr Inhalt ändert, sodass der Entwickler immer einen aktuellen DI-Container hat. Wie gesagt ist das Parsen selbst sehr schnell.

Wie greife ich in meiner Klasse auf die Parameter aus der Konfigurationsdatei zu?

Denken Sie an Regel Nr. 1: Lassen Sie es sich übergeben. Wenn eine Klasse eine Information aus der Konfigurationsdatei braucht, überlegen Sie nicht, wie sich die Klasse diese holen kann. Fordern Sie sie stattdessen einfach an, zum Beispiel über den Konstruktor der Klasse. Und geben Sie diesen Wert dann in der Konfigurationsdatei an.

In diesem Beispiel ist %myParameter% ein Platzhalter für den Wert des Parameters myParameter, der dem Konstruktor von MyClass übergeben wird:

# config.neon
parameters:
	myParameter: Irgendein Wert

services:
	- MyClass(%myParameter%)

Wenn Sie mehrere Parameter übergeben oder Autowiring nutzen wollen, ist es sinnvoll, die Parameter in ein Objekt zu packen.

Unterstützt Nette das Container-Interface PSR-11?

Der Nette DI Container unterstützt PSR-11 nicht direkt. Wenn Sie aber Interoperabilität zwischen dem Nette DI Container und Bibliotheken oder Frameworks brauchen, die das PSR-11 Container Interface erwarten, können Sie einen einfachen Adapter schreiben, der als Brücke zwischen dem Nette DI Container und PSR-11 dient.

Was bedeuten die Begriffe Container, Compiler, Definition und so weiter?

Ein kurzes Wörterbuch der Begriffe, die rund um Nette DI immer wieder auftauchen, die meisten davon beim Schreiben von Extensions:

  • Container – das kompilierte Objekt (Nette\DI\Container), das Services bei Bedarf erzeugt und sie zur Laufzeit hält. Es wird einmal als optimierter PHP-Code erzeugt.
  • Compiler – die Maschinerie, die Konfigurationsdateien und Extensions in diese Klasse des Containers verwandelt.
  • ContainerBuilder – das veränderliche Modell des Containers, das während der Kompilierung verwendet wird; es hält die Definitionen der Services, bevor ein einziger echter Service existiert. Siehe Extensions erstellen.
  • Service – ein Objekt, das der Container verwaltet, üblicherweise einmal erzeugt und geteilt (ein Singleton) – eine Datenbankverbindung, ein Mailer, ein Logger.
  • Definition – das Rezept für einen Service: sein Typ, wie er zu erzeugen ist und was danach zu tun ist. Nette verwandelt Definitionen in die Factory-Methoden des Containers; es gibt mehrere Arten (siehe Typen von Definitionen).
  • Typ – die Klasse oder das Interface eines Services, anhand derer das Autowiring Services den Stellen zuordnet, die sie verlangen.
  • Autowiring – das automatische Übergeben von Services an Konstruktoren und Methoden anhand ihres Typs, sodass Sie Abhängigkeiten nicht von Hand verdrahten.
  • Tag – eine Markierung an einer Definition (wahlweise mit einem Wert); eine Extension kann dann alle Services mit dieser Markierung über findByTag() finden.
  • Setup – zusätzliche Aufrufe, die unmittelbar nach dem Erzeugen auf einem Service ausgeführt werden – Methodenaufrufe oder Zuweisungen an Properties, ergänzt über addSetup().
  • Alias – ein alternativer Name für einen bestehenden Service.
Version: 3.x