Websiteentwicklung: PHP: Dependency Injection
Dependency Injection
BearbeitenDependency Injection - zu deutsch etwa „Abhängigkeits-Injektion“ - beschreibt ein Verfahren, die von einer Klasse benötigten Objekte (Abhängigkeiten) von außen in die Klasse zu injizieren. Dies ist der Neigung vieler Anfänger entgegengesetzt, Objekte oftmals direkt in der Klasse zu instanziieren (erstellen).
Beispiel direkter Instanzierung (keine Dependency Injection):
class MyController {
private $logger;
public function __contruct() {
$this->logger = new Logger();
}
public function myAction() {
$this->logger->log("someone called myAction");
}
}
Das Beispiel zeigt einen Controller MyController, welcher über eine Methode myAction verfügt. Bei Aufruf dieser Methode soll someone called myAction geloggt werden. Der hierfür verantwortliche Logger mit der Klasse Logger wird im Konstruktor von MyController instanziiert. Somit hängt MyController von der Klasse Logger mit allen Ihren potentiellen Eigenheiten ab. Dies ist u. a. aus folgenden Gründen nicht erstrebenswert:
- Verändert sich beispielsweise die Instanziierung der Klasse Logger (z. B. weil Argumente benötigt werden), muss die Klasse MyController ebenfalls angepasst werden
- Verändern sich die Methoden der Klasse Logger, müssen auch diese in MyController angepasst werden
- Soll das Logger-Objekt über Methoden konfiguriert werden, müsste auch dies direkt in MyController eingebaut werden
Des Weiteren ist es strenggenommen nicht die Aufgabe von MyController, sich um die Erstellung anderer Klassen zu kümmern (außer es würde sich um eine Klasse mit Factory-Methoden handeln). Es ist allgemein best practice, lediglich eine einzige Verantwortlichkeit (Responsibility) pro Klasse zu haben.
Um nun den o. g. Missständen entgegenzuwirken, können wir Dependency Injection nutzen. Auch hier gibt es mehrere Arten, das Problem zu lösen - je nachdem, wie abhängig unsere Klasse wirklich von der zu injizierenden Abhängigkeit ist.
Möglichkeit 1: Constructor Injection
BearbeitenAnwendungsfall: Wenn jede Instanz der Klasse definitiv eine Instanz der Abhängigkeit benötigt oder die Abhängigkeit bereits im Konstruktor vorhanden sein muss (z. B. weil im Konstruktor Methoden aufgerufen werden, welche die Abhängigkeit benötigen).
Beschreibung: Bei der Constructor Injection wird die Abhängigkeit als Konstruktorargument übergeben. Auf unser Beispiel bezogen hieße das, die MyController-Klasse so umzubauen, dass unsere Abhängigkeit (die Logger-Klasse) übergeben wird:
class MyController {
private $logger;
public function __construct(Logger $logger) {
$this->logger = $logger;
}
public function myAction() {
$this->logger->log("someone called myAction");
}
}
An der Stelle, wo MyController instanziiert wird, muss nun eine Instanz der Klasse Logger übergeben werden:
$logger = new Logger();
$myController = new MyController($logger);
Das wirkt nun erstmal wenig speaktakulär. Vorteil ist hier jedoch, dass die Instanziierungslogik für die Klasse Logger nicht mehr in MyController benötigt wird. Wir haben MyController also bereits schlanker gemacht. Wollten wir nun z. B. den Logger noch konfigurieren, könnten wir das auch an dieser Stelle vornehmen:
$logger = new Logger();
$logger->setTargetPath('/my/log.txt');
$logger->setLogLevel(2);
$myController = new MyController($logger);
Ansonsten hätten wir auch die Aufrufe der Methoden setTargetPath und setLogLevel in MyController platzieren müssen, was die Anfälligkeit dieser Klasse für Änderungen signifikant erhöht hätte. Hierzu ein weiteres Beispiel: Würde sich der Aufruf der Methode setLogLevel ändern und hätten wir diese in MyController aufgerufen, müssten wir nicht nur die Logger-Klasse, sondern auch MyController anpassen. Hätten wir mehrere Controller-Klassen, in welchen wir genauso verfahren wären, müssten auch dort die Anpassungen vorgenommen werden.
Möglichkeit 2: Setter Injection
BearbeitenAnwendungsfall: Wenn die Abhängigkeit an bestimmten Stellen benötigt wird und vorher bekannt ist, ob das der Fall ist. Des Weiteren bei Abhängigkeiten, die austauschbar sein sollen oder in Fällen, wo Constructor Injection nicht genutzt werden kann/soll.
Beschreibung: Bei dieser Form der Dependency Injection wird die Abhängigkeit über eine Setter-Methode hinzugefügt.
$logger = new Logger();
$myController = new MyController();
$myController->setLogger($logger);
Die Klasse MyController sähe wie folgt aus:
class MyController {
private $logger;
public function setLogger(Logger $logger) {
$this->logger = $logger;
}
public function myAction() {
$this->logger->log("someone called myAction");
}
}
Das Logger-Objekt wird also einfach über die Methode setLogger gesetzt. Dies ist z. B. dann sinnvoll, wenn die Abhängigkeit ein großes Objekt ist und nur in ausgewählten, vorher bekannten Fällen benötigt wird. Wie weiter oben bereits beschrieben, muss darauf geachtet werden, dass die Abhängigkeit erst nach dem Aufruf der Setter-Methode (hier setLogger) von der Klasse benötigt wird, da sie vorher in der Klasse nicht vorhanden ist.
Vor- und Nachteile
BearbeitenDer kritische Leser mag sich nun fragen, wo denn überhaupt der Vorteil der Dependency Injection liegt, haben wir doch lediglich die für das Logger-Objekt notwendige Logik an eine andere Stelle verschoben. Auf unser einfaches Beispiel bezogen ist dieser Einwand durchaus berechtigt, denn etwaige Änderungen der Logger-Klasse führen ggf. immer noch zu Änderungen an den Stellen, wo sie verwendet wird. Deshalb ist es notwendig, die Stelle der Instanziierung achtsam zu wählen. Beispielsweise könnte hierfür eine Stelle mit vielen verwandten Objektinstanziierungen oder gar eine Factory verwendet werden.
Dennoch gibt es bereits jetzt Vorteile:
- Die Klasse MyController muss nichts über die Instanziierung von Logger-Objekten wissen
- Wir könnten je nach Anwendungsfall Objekte der Logger-Klasse unterschiedlich konfigurieren, bevor sie via Dependency Injection in MyController eingefügt werden
- Hätten wir mehrere Controller-Objekte, könnten wir stets die gleiche Logger-Instanz übergeben (bessere Performance)
- Das Testen der MyController-Klasse würde nun die Verwendung von speziellen Mock-Objekten für den Logger erlauben
Es gibt allerdings immer noch einen gravierenden Nachteil: Die Klasse MyController ist von der Methode log der Logger-Klasse abhängig. Änderungen an dieser Methode würden u. U. Änderungen an der MyController-Klasse nach sich ziehen. Genau aus diesem Grund gibt es die sog. Interface Injection.
Möglichkeit 3: Interface Injection
BearbeitenDurch das bisherige Type Hinting im Konstruktor (Contructor Injection) bzw. der setLogger-Methode (Setter Injection) in der MyController-Klasse haben wir diese an die Logger-Klasse gebunden. Sie muss nun zwar nicht mehr die Details der Instanziierung kennen, sich aber immer noch an den von der Logger-Klasse zur Verfügung gestellten Methoden orientieren. Nun möchten wir diese Abhängigkeit auch noch auflösen.
Anwendungsfall: Vermeidung der Abhängigkeit von konkreten Klassen (z. B. falls diese nicht bekannt sind und keine gemeinsame Parent-Klasse haben).
Beschreibung: Wir könnten auch einfach das Type Hinting an entsprechenden Stellen komplett entfernen - dies hieße dann Duck Typing. So würde jedoch die Fehleranfälligkeit erhöht: Es könnte jedes Objekt übergeben werden und erst bei dem Aufruf einer Methode, die das übergebene Objekt nicht kennt, würde ein Fehler sichtbar werden (der dann noch zurückverfolgt werden müsste). Stattdessen sollten wir uns überlegen, was MyController eigentlich von Logger will. In unserem Fall wollen wir nur Loggen, mehr nicht. Und zwar über eine log-Methode, die das, was wir loggen wollen, als Argument übergeben bekommt. Es ist unserer Klasse MyController völlig egal, ob der Logger, den sie nutzt, noch über Methoden wie setLogLevel oder ähnliche verfügt. Sie ist nur am einfachen Loggen über eine log-Methode interessiert. Wir können also eine Schnittstelle (Interface) zwischen MyController und der Logger-Klasse definieren:
interface LoggerInterface {
public function log($message);
}
Dieses muss nun von der Logger-Klasse implementiert werden.
Nun können wir die Type Hints austauschen: Logger wird zu LoggerInterface:
class MyController {
private $logger;
public function setLogger(LoggerInterface $logger) {
$this->logger = $logger;
}
public function myAction() {
$this->logger->log("someone called myAction");
}
}
Diese Klasse hat nun keine Abhängigkeit mehr zur Logger-Klasse. Jedes Objekt, dessen Klasse das LoggerInterface implementiert, kann hier übergeben werden. Würde die Klasse Logger nun die log-Methode ändern, müsste sie sicherstellen, dass diese kompatibel zu dem von ihr implementierten Interface bleibt. Des Weiteren hätten wir die Möglichkeit, Objekte der MyController-Klasse je nach Bedarf mit ganz unterschiedlichen Loggern auszustatten, die lediglich das LoggerInterface implementieren müssen.