Spätestens seit der stetig wachsenden Popularität von Unittests, besonders durch PHPUnit, bekam die Auflösung der Abhängigkeiten von Klassen durch Dependency Injection in PHP eine immer größere Rolle im Alltag der Entwickler. Klassen, welche eine oder mehrere Abhängigkeiten zu anderen Klassentypen haben, bekommen diese bei ihrer Erzeugung über den Konstruktor oder zur Laufzeit über Methodenaufrufe übergeben. Dadurch verbessert sich die Wartbarkeit der einzelnen Klassen und Abhängigkeiten lassen sich bei geänderten Anforderungen leichter austauschen. Ebenfalls verbessert sich die Testbarkeit der einzelnen Komponenten, bei der Abhängigkeiten von Klassen durch einfache Test Doubles ersetzt werden können. Ein Nachteil jedoch ist vermehrter Boilerplate Code, wenn an verschiedenen Stellen im Projekt immer die gleichen Objekte erzeugt werden müssen, um die gleichen Abhängigkeiten abzudecken.
Mit den aktuellen PHP-Frameworks wurde der sogenannte Service Locator eingeführt. Die Aufgabe des Service Locator ist die Auflösung und Instantiierung von Abhängigkeiten zu einem Service intern durchzuführen. Dadurch können Objektinstanzen über einen Service erstellt werden, ohne dass dabei der Boilerplate Code zur Auflösung der Abhängigkeiten in der Business Logic hinzugefügt werden muss. Weiterhin sorgt der Service Locator für eine einzige Instanz des aufgerufenen Services innerhalb eines Requests, um unnötige Objekterzeugungen zu vermeiden, ohne dabei die Nachteile eines klassischen Singletons mit sich zu bringen.
Die bequeme Handhabung des Service Locator hat jedoch im Laufe der Zeit die Verwendung von Dependency Injection in Richtungen gelenkt, die zwar technisch schnell umsetzbar sind, aber dessen Sinn nicht ganz folgen. Aus dem Auflösen von Abhängigkeiten wurde ein buntes Zusammenwerfen von Klassen.
Im Folgenden möchte ich kurz die Fälle vorstellen, auf die ich hin und wieder gestoßen bin. Für gewöhnlich wirken diese auf den ersten Blick vielleicht nicht verkehrt, können aber in der Weiterentwicklung Nachteile mit sich bringen.
Service Locator Injection
Die wohl bekannteste Unsitte bei der Verwendung eines Service Locator ist es, diesen als Abhängigkeit in seine Klasse einzufügen. Meist mit dem Argument „Vielleicht brauche ich noch einen der Services“ begleitet, ist dieses Pattern eher ein Zeichen dafür, dass man in seiner Entwicklung nicht genug geplant hat. Keine Klasse braucht jemals alle Services. Manchmal findet man auch über Google Kommentare, die es in Ordnung finden, wenn eine Factory den Service Locator übergeben bekommt. Selbst eine Factory braucht wenige Services um zu funktionieren. Wenn eine Abhängigkeit zu mehr als 4 weiteren Services besteht, sollte nochmals über die Aufgabe und den Aufbau der Factory nachgedacht werden.
Die Komplexität zeigt sich besonders im Schreiben von Unittests. Anhand der Klasse können normalerweise die Abhängigkeiten aus dem Methoden-Kopf von Konstrukor oder Setter-Methoden erkannt werden. Mit dem Hinzufügen eines Service Locator muss nicht nur für diesen ein Test Double erstellt werden, sondern auch für alle tatsächlichen Abhängigkeiten, die aus ihm geholt werden. Es ist ausserdem anhand des Codes schwer zu erkennen, um welchen tatsächlichen Typ es sich bei dem Service handelt. Da in PHP eine Klasse auch Methoden enthalten kann, die nicht aus einem Interface stammen, ist auch die Austauschbarkeit bei Klassen mit dem selben Interface nicht mehr gewährleistet.
Abhängigkeiten sollten optimalerweise übergeben und nicht aus einem anderen Objekt geholt werden.
Wer sich beispielsweise Symfony 2 anschaut, wird erkennen, dass auch hier der Service Locator direkt verwendet wird. Die bekanntesten Beispiele sind die Controller und die Commands, jedoch auch nur, wenn diese das Interface ContainerAware implementieren, was als Standard der Fall ist. Aber auch für Controller und Commands gibt es Möglichkeiten, dies zu vermeiden.
Logging
Das Loggen von Informationen kann für das eine Projekt wichtiger sein als für andere und kann sich in der Menge der Informationen unterscheiden. Deshalb gibt es auch oft den Ansatz eine Logger-Instanz als Abhängigkeit fest in den Service einer Klasse einzufügen. In den wenigsten Fällen ist ein Logger allerdings wirklich eine Abhängigkeit, denn zuvor muss man sich selbst eine Frage stellen: „Funktioniert ohne den Logger eigentlich noch mein Feature?“
Je nach Entwurfsentscheidung kann es reichen innerhalb der Klasse eine Exception zu werfen und in einem Catch-Block neben dem Error-Handling auch einen Log-Eintrag hinzuzufügen.
<?php // ... code try { $container->get('my_sevice')->doSomething();
} catch(Exception $e) {
$container->get('logger')->error($e->getMessage());
}
Eine andere Möglichkeit ist auch das Sammeln der Messages, um diese später ausgeben zu lassen, wie man es z.B. von Validatoren kennt. Auch hier lassen sich ausserhalb der Klasse die Log-Einträge erstellen und somit den Logger als Abhängigkeit herausziehen.
<?php // ... code $errors = $container->get('validator')->validate($entity);
if (0 !== count($errors)) {
foreach ($errors as $message) {
$container->get('logger')->error($message);
}
}
Zugegeben, es ist keine grobe Verletzung von Abhängigkeiten, aber wenn es keine Schwierigkeiten bereitet den Logger aus einer Klasse herauszuhalten, sollte der Entwickler dies tun. Es ist in automatisierten Tests eine unnötige Komponente und für die Funktion der zu testenden Klasse unerheblich.
Zu viele Abhängigkeiten
Wer die vielen Abhängigkeiten im Konstruktor seiner Klasse ansieht und sich damit unwohl fühlt, hat wahrscheinlich das Problem erkannt. Die einfache Handhabung von Service-Konfigurationen erleichtert es neue Abhängigkeiten hinzuzufügen, da sich der Entwickler gewöhnlich nicht um daraus resultierende Änderungen bei der Erstellung von Instanzen kümmern muss. Die Anzahl der Abhängigkeiten kann dadurch auch beim Fortschreiten eines Projekts wachsen und es entsteht eine Komplexität, die ungerne angefasst und geändert wird.
Viele Dependency Injection Container bieten in der Konfiguration die Möglichkeit an, Service-Abhängigkeiten über Setter-Methoden zu setzen. Dadurch tendieren manche Entwickler dazu die Abhängigkeiten anstatt über dem Konstruktor über Setter-Methoden zu setzen, allerdings ist damit nicht das Problem gelöst worden. Im Gegenteil: Setter müssen explizit gesetzt werden und zwar erst dann, wenn das Objekt schon besteht. Die Verwendung der Setter wird von der Service-Konfiguration abgenommen, dennoch kann diese Klasse, z.B. bei Unittests, nur mit dem Wissen von diesen Settern verwendet werden und wenn Pflicht-Abhängigkeiten nicht gesetzt sind, muss der Entwickler sich auch um ein Error-Handling kümmern. Das vergrößert die betroffene Klasse unnötigt. Das alles ist bei Dependency Injection über den Konstruktor nicht notwendig. Wie soll also mit diesem Problem umgegangen werden?
Es ist ein gutes Zeichen dafür, dass die Klasse mehr macht, als sie eigentlich tun sollte.
In diesem Fall müssen ihre Aufgaben aufgeteilt werden, welche dann die tatsächlichen Abhängigkeiten der ursprünglichen Klasse darstellen. Es ist auch nicht auszuschließen, dass die Ursprungsklasse selbst nicht mehr gebraucht wird und somit entfällt, da ihre Aufgabe nur noch darin besteht, die Aufgaben ihrer Abhängigkeiten auszuführen.
Auflösung von Abhängigkeiten
Nicht jede Abhängigkeit ist zwingend über die Service-Konfiguration zu lösen geschweige denn überhaupt eine direkte Abhängigkeit für eine Klasse. Trotzdem werden diese über den Konstruktor per Service eingefügt. Durch die einfache und schnelle Konfiguration von Services sind Änderungen schnell durchgeführt ohne dabei die Stellen im Code ändern zu müssen, die den Service aufrufen. Das lädt einen Entwickler dazu ein alles über die Service-Konfiguration für den betroffenen Service zu lösen.
Im Folgenden Beispiel hat die Klasse Foo eine Abhängigkeit zu $myService, die im Konstruktor übergeben wird.
<?php class Foo { private $myService; private $otherService; private $importantService;
public function __construct($myService, $otherService, $importantService)
{ $this->myService = $myService;
$this->otherService = $otherService;
$this->importantService = $importantService;
}
public function bar()
{
// some code
}
public function doSomething()
{
// some code
$this->myService->doMore($bar);
// more code and return value
}
public function doBaz()
{
// some code
}
}
Im Beispiel wird diese Abhängigkeit jedoch nur in der Methode doSomething verwendet. Wird die Abhängigkeit nicht von der Klasse selbst gebraucht, sondern nur von einer ihrer Methoden, sollte überlegt werden ob vielleicht nicht der folgende Ansatz eine bessere Lösung darstellt:
<?php class Foo { private $otherService; private $importantService;
public function __construct($otherService, $importantService)
{ $this->otherService = $otherService;
$this->importantService = $importantService;
}
public function bar()
{
// some code
}
public function doSomething($myService)
{
// some code
$myService->doMore($bar);
// more code and return value
}
public function doBaz()
{
// some code
}
}
Nachteil: Die Klassen, die Foo und dessen Methode doSomething verwenden, hat somit eine Abhängigkeit zu $myService bekommen.
Als zweiter Ansatz muss ebenfalls die Überlegung gemacht werden, ob die Aufgabe der Methode doSomething nicht in eine eigene Klasse gehört und als eigener Service konfiguriert werden muss. Dieser neue Service muss allerdings nicht zwingend eine Abhängigkeit in der Klasse Foo sein.
<?php class Foo { private $otherService; private $importantService;
public function __construct($otherService, $importantService)
{ $this->otherService = $otherService;
$this->importantService = $importantService;
}
public function bar()
{
// some code
}
public function doBaz()
{
// some code
}
}
// new class with former Foo method doSomething
class Qux
{
private $myService;
public function __construct($myService)
{
$this->myService = $myService;
}
public function doSomething($bar)
{
// some code
$myService->doMore($bar);
// more code and return value
}
}
// ...
// use services of Foo and Qux
$bar = $container->get('foo')->bar();
$result = $container->get('qux')->doSomething($bar);
// ...
In diesem Beispiel-Code wurde die Aufgabe der Methode doSomething komplett aus der Klasse Foo herausgenommen und in einer neuen Klasse „Qux“ implementiert, welche als Service eine Abhängigkeit zu $myService des vorangegangenen Beispiels hat.
Natürlich ist dieses Beispiel einfach gehalten, denn im Fall eines Refactorings stehen hier auch andere Hürden im Weg. Mit dem Ändern der Klassenstruktur müssen an allen Stellen, welche Foo verwendet haben, die Änderungen nachgezogen werden. Noch schwieriger wird eine Änderung der Klassenstruktur, wenn diese von einem Interface vorgegeben ist.
Dependency Injection und Services
Dependency Injection ist ein Überbegriff, der viele verschiedene Umsetzungsformen kennt. Die Entscheidung, welche der Formen in einem konkreten Fall angewendet werden sollen, kann uns nicht ein Dependency Injection Container bzw. Service Locator abnehmen, sondern muss vom Entwickler selbst entschieden werden. Frameworks bieten uns Funktionalitäten an, die uns bei der Entwicklung helfen und viele langwierige Schritte abnehmen sollen. Wie es eingesetzt wird, bleibt immer noch uns überlassen.
0 Kommentare