Dependency Injection - Eine praktische Einführung

Von am

Obwohl das Thema Dependency Injection (DI) bereits seit vielen Jahren bekannt und auch verbreitet ist, fallen vielen Anfängern die ersten Schritte mit diesem Thema schwer. Das liegt nicht zuletzt daran, dass dabei oft der Einsatz von Frameworks eine nicht zu unterschätzende Hürde darstellen kann und eine schnelle Lösung einer sauberen Lösung vorgezogen wird. Dabei geht es bei Dependency Injection gar nicht um Frameworks, sondern darum wie wir die Abhängigkeiten in unseren Systemen erkennen und besser verwalten.

Um das direkt am Anfang klar zu stellen: In diesem Artikel werden wir nicht den Einsatz von Dependency Injection Containern z.B. von Symfony oder Zend Framework erarbeiten, sondern wie man ganz praktisch Klassen bis hin zu einer ganzen Anwendung unter dem Gesichtspunkt der Trennung von Abhängigkeiten aufbaut.

Aber klären wir zunächst ein paar grundsätzliche Fragen.

Was ist eigentlich das Problem?

Wie üblich bei gängigen Prinzipien aus der Softwareentwicklung liegt dem Prinzip ein wiederkehrendes Problem zugrunde. Schauen wir uns dazu folgende Klasse an.

<?php

namespace Example;

class UglyCoupling
{
    private $handle;

    public function __construct() {
        $filename = 'output.txt';
        $handle = @fopen($filename, 'w');
        if (!$handle) {
            throw new \RuntimeException('Unable to open output file!');
        }
        $this->handle = $handle;
    }

    public function updateOrder($orderId, $newValue) {
        // ... update the order
        $output = 'Order ' . $orderId . ' has been changed to ' . $newValue . '!';
        $bytes = fwrite($this->handle, $output);
        if ($bytes === false) {
            throw new \RuntimeException('Unable to write to output file!');
        }
    }
}

Welche Probleme hat diese Klasse?

  • Der Dateiname ist nicht änderbar
  • Es ist nicht klar, wo im Dateisystem die Datei ggf. erzeugt wird
  • Die Klasse UglyCoupling kann nicht genutzt werden, wenn die Datei nicht geschrieben werden kann
  • Die Klasse wirft Exceptions bei Fehlern, die mit ihrer eigentlichen Funktion nichts zu tun haben
  • Wenn weitere Dinge beim Aufruf von "updateOrder()" passieren sollen, müssen sie in "UglyCoupling" hinzugefügt werden
  • Tests erzeugen Dateien im Dateisystem, auch wenn das auf dem Testsystem vielleicht gar nicht gewünscht ist
  • Die Datei wird nicht geschlossen (bzw. erst, wenn der PHP-Prozess endet)
  • Jede Instanz der Klasse UglyCoupling öffnet die selbe Datei
  • Wenn der Inhalt des "outputs" oder das Ausgabeformat von Text auf PDF geändert werden soll, muss die Klasse UglyCoupling angepasst werden

Was ist Dependency Injection?

Dependency Injection fordert uns auf Ressourcen anzufordern, statt sie selber bereit zu stellen. Es gibt drei Wege Ressourcen anzufordern:

  1. Constructor Injection
  2. Setter Injection
  3. Interface Injection

Constructor Injection: Klassen, die Ressourcen für ihre Arbeit benötigen fordern diese Ressourcen über den Konstruktor an. Damit ist gewährleistet, dass die Klasse von Beginn an über die Ressourcen verfügt, die sie benötigt.

<?php

class ConstructorInjection
{
    /**
     * @var Service
     */
    private $service;

    public function __construct(Service $service)
    {
        $this->service = $service;
    }
}

Ein klarer Nachteil der Constructor Injection ist, dass sehr früh im Application Lifecycle Ressourcen bereitgestellt werden, die evtl. gar nicht zum Einsatz kommen.

Eine Konsequenz aus Constructor Injection ist, Ressourcen so kostengünstig wie möglich zu erstellen, damit keine überflüssige Arbeiten vorgenommen werden.

Setter Injection: Benötigte Ressourcen werden über Set-Methoden bereitgestellt. Diese Methoden können ggf. deutlich nach der Erstellung des Anfordernden Objekts aufgerufen und dem Klienten bereitgestellt werden. Dabei besteht aber auch das Risiko einen ungültigen Zustand, ein unvorhersehbares Verhalten oder sogar einen Fehler hervorzurufen.

<?php

class SetterInjection
{
    /**
     * @var Service
     */
    private $service;

    /**
     * @param Service $service
     */
    public function setService(Service $service)
    {
        $this->service = $service;
    }
}

Interface Injection: Benötigt eine Klasse eine bestimmte Ressource, dann implementiert sie ein Interface, welches das Injizieren dieser Ressource anfordert. D.h. das Interface fordert die Implementierung z.B. einer "inject"-Methode, die als Parameter die entsprechende Ressource erwartet.

<?php

class InterfaceInjection implements InjectService
{
    /**
     * @var Service
     */
    private $service;

    public function injectService(Service $service)
    {
        $this->service = $service;
    }

}

Der Unterschied zwischen Setter Injection und Interface Injection scheint auf den ersten Blick gering und der zusätzliche Aufwand nicht gerechtfertigt. Interface Injection eignet sich aber dann besonders gut, wenn ein System z.B. durch Reflection (also die Laufzeitanalyse von Klasseneigenschaften) seine Komponenten zusammenbaut, oder eine Unterscheidung von "normalen" Settern gewünscht und sinnvoll ist.

Was ist eigentlich mit einer "Ressource" gemeint?

Eine Ressource kann eigentlich alles mögliche sein - von einer einfachen Zahl bis zu einer komplexen Service-Klasse.

Es ist also keines Falls so, dass nur Services über Dependency Injection angefordert werden sollen. Ein klassisches Beispiel dafür sind Parameter für eine Datenbankverbindung.

Aufgabe: Überarbeiten sie "UglyCoupling"

Bitte überarbeiten Sie "UglyCoupling" so, dass die benötigten Ressourcen angefordert werden.

Ist das folgende Ergebnis besser?

<?php

class LessUglyCoupling
{
    /**
     * @var \SplFileObject
     * @internal
     */
    private $fileObject;

    public function __construct(\SplFileObject $fileObject)
    {
    $this->fileObject = $fileObject;
    }

    public function updateOrder($orderId, $newValue)
    {
        // ... update the order
        $output = 'The value of order ' . $orderId . ' has been changed to ' . $newValue . '!';
        $bytes = $this->fileObject->fwrite($output);
        if ($bytes === null) {
            throw new \RuntimeException('Unable to write to output file!');
        }
    }
}

Naja, zumindest kann man jetzt den Dateinamen ändern, aber es wird immer noch in eine Datei ein Text geschrieben. Außerdem liefert die Funktion "fwrite()" im Fehlerfall einen anderen Rückgabewert als die Methode SplFileObject::fwrite() (nämlich "false" statt "null")!

Darüber hinaus hat sich aber auch das Verhalten unseres Konstruktors geändert: Er wirft plötzlich keine Exception mehr! Für den Fall, dass man UnitTests für diese Klasse geschrieben hat, müssen wir diese spätestens jetzt anpassen.

Eine Sauberere Lösung mit Konsequenzen!

Was halten Sie von folgender Lösung? Listen Sie Vor- und Nachteile auf!

<?php

class BetterCoupling
{
    /**
     * @var UpdateOrderHandler
     * @internal
     */
    private $updateOrderHandler;

    public function __construct(UpdateOrderHandler $handler)
    {
        $this->updateOrderHandler = $handler;
    }

    public function updateOrder($orderId, $newValue)
    {
        // ... update the order
        $this->updateOrderHandler->onOrderUpdate($orderId, $newValue);
    }
}

Vorteile:

  • Die Klasse "BetterCoupling" muss nicht mehr entscheiden, was beim Aufruf von "updateOrder" alles passiert. Diese Entscheidung trifft nun die Klasse "UpdateOrderHandler".
  • Wir müssen uns nicht mehr um die Behandlung von Fehlern kümmern, die uns eigentlich gar nicht interessieren.
  • Es ist kein fester Text mehr vorhanden.

Nachteile:

  • Wir brauchen eine zusätzliche Klasse "UpdateOrderHandler" und haben damit eine neue Abhängigkeit eingeführt.
  • Sollte man sich (idealer Weise) entschieden haben, dass "UpdateOrderHandler" ein Interface ist, dann haben wir dem System sogar noch weitere PHP-Dateien hinzugefügt.
  • Es muss auf jeden Fall ein Handler vorhanden sein, oder wir müssen die Implementierung wieder anpassen, so dass die Methode "onOrderUpdate()" nicht aufgerufen wird, wenn es keinen Handler gibt.

Die beste Lösung!?

Noch besser? Ist es möglich, dass wir ohne Abhängigkeiten auskommen?

<?php

class NoCoupling
{
    public function updateOrder($orderId, $newValue)
    {
        // ... update the order
    }
}

Und was ist mit unserer Ausgabedatei?

Machen wir das doch einfach so:

<?php

class CouplingByInheritance extends NoCoupling
{
    // ...

    public function updateOrder($orderId, $newValue)
    {
        parent::updateOrder($orderId, $newValue);
        // ... now do whatever you need to do!
    }
}

Diese Klasse kann von uns frei gestaltet werden, ohne dass wir die zugrunde liegende Implementierung des fachlichen Problems ändern müssen. Die Klasse "NoCoupling" kümmert sich also nur noch um das fachliche Problem und die abgeleitete Klasse kann irgend eine der bisher gezeigten Lösungsvarianten umsetzen.

## Wie werden die ganzen Ressourcen zusammengesetzt?

Das ist ja alles ganz schön, aber jetzt haben wir ein anders Problem: Ich habe nichts gewonnen, wenn jetzt an der Stelle, an der ich früher "UglyCoupling" eingesetzt habe, eine ganze Reihe von anderen Klassen zusätzlich erzeugen muss!

Wo früher

<?php

class MyUglyApplication
{
    public function doSomething()
    {
        // ...

        $oderId = $this->providerOrderId();
        $newValue = $this->providerNewValue();

        $myUglyClass = new UglyCoupling();
        $myUglyClass->updateOrder($oderId, $newValue);
    }
}

stand, steht jetzt (oder je nachdem für welche Implementierung wir uns entschieden haben)

<?php

class EvenMoreUglyApplication
{
    public function doSomething()
    {
        // ...

        $filename = 'output.txt';
        $fileObject = new \SplFileObject($filename);
        $updateOrderHandler = new UpdateOrderHandler($fileObject);

        $oderId = $this->providerOrderId();
        $newValue = $this->providerNewValue();

        $myUglyClass = new CouplingByInheritance($updateOrderHandler);
        $myUglyClass->updateOrder($oderId, $newValue);
    }
}

Im Client-Code (also unserer Anwendung) ist es scheinbar nicht besser sondern schlimmer geworden. Der Code sieht darüber hinaus unleserlich aus.

Aber durch einfaches Refactoring lassen sich sehr schöne und saubere Methoden erstellen, die die einzelnen Ressourcen erstellen:

<?php


class MyInjectingApplication
{
    public function doSomething()
    {
        // ...

        $oderId = $this->providerOrderId();
        $newValue = $this->providerNewValue();

        $coupledClass = $this->getCoupledClass();
        $coupledClass->updateOrder($oderId, $newValue);
    }

    /**
    * @return CouplingByInheritance
    */
    protected function getCoupledClass()
    {
        $updateOrderHandler = $this->getUpdateOrderHandler();
        $coupledClass = new CouplingByInheritance($updateOrderHandler);
        return $coupledClass;
    }

    /**
    * @return UpdateOrderHandler
    */
    protected function getUpdateOrderHandler()
    {
        $fileObject = $this->getFileObject();
        $updateOrderHandler = new UpdateOrderHandler($fileObject);
        return $updateOrderHandler;
    }

    /**
    * @return \SplFileObject
    */
    protected function getFileObject()
    {
        return new \SplFileObject($this->getFilename());
    }

    /**
    * @return string
    */
    protected function getFilename()
    {
        return 'output.txt';
    }
}

Auf diese Weise bleibt der Code leserlich, die Erstellung jeder einzelnen Ressourcen ist in jeweils einer Methode abgebildet und aus welchen Sub-Ressourcen eine angeforderte Ressource zusammengesetzt ist, kann jederzeit ganz gezielt geändert werden.

Der letzte verbleibende Schritt an dieser Stelle wäre zu entscheiden, welche der Ressourcen immer wieder aufs Neue oder nur einmal erstellt werden sollen.

Dependency Injection und Anwendungen

Während das gezeigte Beispiel den Ansatz verfolgt, dass die Anwendung selbst der Dependency Injection Container (also die Komponente, welche das System "zusammensetzt") ist, gibt es natürlich eine Reihe von Frameworks, welche diese Aufgabe durch Konfigurationsdateien erledigen.

Interessant dabei ist, dass der jeweilige Dependency Injection Container (DIC) in der Regel die Konfigurationsdatei "auscompiliert", also in eine ausführbare Datei umwandelt, welche tatsächlich sehr ähnlich der oben vorgestellten Lösung ist.

In jedem Fall ermöglicht einem der oben vorgestellte Ansatz zu einem späteren Zeitpunkt die Anwendung relativ einfach auf einen DIC eines Frameworks umzustellen - die beteiligten Klassen fordern ja bereits ihre benötigten Ressourcen an, statt sie selber zu erstellen.

Aufgabe: Anwendungen "Komponieren"

Eine Anwendung zu Verwaltung von Rechnungen soll als einen Anwendungsfall folgendes Verhalten umsetzen:

Wenn der Anwender den Prozess "Rechnung Erstellen" (generateBill) aufruft wird

  • ein PDF der Rechnung erstellt und
  • ein Rechnungsbericht mit der Anzahl der Seiten der Rechnung ins Logfile geschrieben
werden.

Hinweise:

  • Erstelle "Kommandos" (Commands, siehe Command-Pattern), die Aufgaben kapseln
  • Lasse Commands die benötigten Ressourcen anfordern
  • Abstrahiere Teilinformationen (z.B. die Anzahl der Seiten des PDFs)