DI-Framework meets onOffice

Wir diskutieren aktuell den Einsatz eines DI-Frameworks.

Unser Hauptprodukt onOffice enterprise hat mittlerweile ca. 14.000 Klassen und hatte das Ziel, Klassen möglichst klein und überschaubar zu halten (single-responsibility / clean-code). Daraus ergeben sich immer größere Initialisierungsketten. Im Unit-Testing stehen wir vor der Herausforderung, dass möglichst alle Bestandteile von außen per Injection übergeben werden sollen. Klassenstrukturen müssen dann aber unter Umständen die Bestandteile unendlich weiterreichen. Hier setzt das DI-Framework an.

Was ist eigentlich ein DI?

Dependency Injection, kurz DI, ist ein Entwurfsmuster (Design Pattern) in der objektorientierten Programmierung, das die Abhängigkeiten eines Objekts zur Laufzeit reglementiert. Die Bezeichnung wurde von Martin Fowler im Jahre 2004 eingeführt, um den damaligen Begriff von “Inversion of Control” zu präzisieren. Mit DI können “Hardcoded Dependencies” vermieden werden und die Basis für alle Kompositions-Pattern gebildet werden.

Oft beschreibt ein DI die Verantwortung, mit der Objekte mithilfe einer eigenständigen Komponente erzeugt und verknüpft werden. Genau genommen ist dies jedoch ein DI-Framework. DI bedeutet im eigentlichen Sinne nur, dass Objekte per Komposition zusammengebaut werden oder Methoden Objekte übergeben bekommen, deren Funktionalität verwendet werden soll.

Es gibt zwei Varianten der DI:

  1. Übergabe von externen Abhängigkeiten an den Konstruktor (A benötigt B)
  2. Übergabe von externen Abhängigkeiten an eine Methode (A benutzt B)

Pauschal sollte die Konstruktor-Variante gewählt werden, wenn die externe Abhängigkeit speziell für diesen Zweck erstellt wurde und “danach nicht mehr gebraucht wird” (oder: A benötigt B).

Die Methoden-Variante eignet sich, wenn die externe Abhängigkeit das Objekt “überleben” wird (oder soll) (oder: A benutzt B).

Was gibt es bereits?

Die Kanban-Kernpraktik “Führe Verbesserungen kontinuierlich basierend auf Methoden und Modellen durch”  lässt uns im ersten Schritt über den Tellerrand schauen. Welche bestehenden DI-Frameworks gibt es? Wo müssen wir das Rad erst gar nicht mehr neu erfinden?

In der PHP-Welt gibt es zum Beispiel PHP-DI, pimple, aura.DI, zend-DI als fertige DI-Frameworks. In der Regel werden externe Abhängigkeiten in einem zentralen Container gespeichert.

Abhängigkeiten werden meist formal deklariert

  • über Konfigurationsdateien (XML, YAML, JSON, etc.)
  • über DocBlocks (ausgelesen über Reflection), z.B. @inject
  • über Type-Hinting (ausgelesen über Reflection)

Objekte werden von einer zentralen Factory erstellt, die Zugriff auf die Abhängigkeiten und den Container hat:

// Beispiel 1
class E { public function __construct() {} }
class D { public function __construct(E $pE) {} }
class C { public function __construct(D $pD) {} }
class B { public function __construct(C $pC) {} }
class A { public function __construct(B $pB, C $pC, E $pE) {} }

// Beispiel 2 (mit zyklischer Abhängigkeit)
class G { public function __construct(H $pH) {} }
class H { public function __construct(G $pG) {} }

In Beispiel 1 soll ein Objekt der Klasse A erstellt werden.
In Beispiel 2 soll ein Objekt der Klasse H erstellt werden.

Die Instanziierung von H (oder G) kann* nicht funktionieren: Henne-Ei-Problem.

* stimmt nicht ganz, die Methode Reflection::newInstanceWithoutConstructor kann Objekte erstellen, ohne den Konstruktor aufzurufen
-> pfui!

Zur Anschauung hier der herkömmliche – umständliche – Weg:

// Beispiel 1
$pE = new E;
$pD = new D($pE);
$pC = new C($pD);
$pB = new B($pC);
$pA = new A($pB, $pC, $pE);

echo $pA instanceof A ? 'YES' : 'NO'; // gibt YES aus

// Beispiel 2
$pH = new H(new G(new H(new G(new H(new G(new H …

// Erstellung eines Objekts der Klasse H geht einfach nicht,
// es sei denn, man schafft es in die Unendlichkeit

$pDi = new Di;

// Beispiel 1
$pA = $pDi->get(A::class);

echo $pA instanceof A ? 'YES' : 'NO'; // gibt YES aus

// Beispiel 2
$pH = $pDi->get(H::class); // geht halt nicht

Ein DI-Framework löst sämtliche Abhängigkeiten von class A korrekt auf, erstellt entsprechende Objekte und injiziert sie in den jeweiligen Konstruktor.

Das automatische Auflösen der Abhängigkeiten ohne besondere (oder notwendige) Konfiguration nennt man “Autowiring”.

Die Instanziierung von H (oder G) kann nicht funktionieren, auch nicht mit einem DI Framework.

PHP-DI (php-di.org) sagt, dass 80% der Fälle per “Autowiring” abgehandelt werden, es also keiner weiteren Anstrengung (Konfiguration oder Code) bedarf, um Objekte vom DI-Framework erzeugen zu lassen

Der zweit häufigste Anwendungsfall ist das Mapping von Interfaces auf konkrete Klassen:

  • class A { function __construct(B $pB) { … } }, wobei B ein Interface darstellt 
  • Erstellung von A mittels Autowiring ist nicht möglich, weil das DI-Framework nicht weiß, welche Klasse für B instanziiert werden soll -> Konfiguration notwendig
  • die einfachste (und wohl auch sinnvollste) Konfiguration wäre vermutlich ein Mapping:
    $config = [ B::class => ConcreteB::class ];

Manche Frameworks bieten auch die Möglichkeit von Mini-Factories (z.B. als Closure) an:

  • $config = [ B::class => function ($pContainer) { return new ConcreteB(); } ];

Die meisten Frameworks erlauben das Ablegen von beliebigen Daten im Container

  • $config = [ ‘db.host’ => env(‘DB_HOST’, ‘default value’) ];
  • oder auch $container['random_func'] = function () { return rand(); }

Kurz gesagt entwickeln sich die meisten existierenden DI-Frameworks zu eierlegenden Wollmilchsäuen. Weg vom eigentlichen Plan, Objekte automatisiert inklusive aller Abhängigkeiten zu erzeugen, hin zu einer Datenmüllhalde, die jeder willkürlich manipulieren oder für seine Zwecke missbrauchen kann.

In unserer Diskussion ist das aber nicht der Weg, den wir gehen wollen.

DI-Vorteile, die wir nutzen wollen

  • Objekte im DI-Container werden nur ein einziges Mal instanziiert, sind also funktional gesehen Singletons
  • static wird nicht mehr benötigt, da alle Klassen, die von einer anderen Klasse abhängen, Zugriff auf das identische Objekt (mit all seinen Informationen) dieser Klasse haben
  • Objekte im DI-Container dürfen keinen Zustand haben, der sich im Verlauf des Requests verändert und die Funktionsweise der Klasse beeinflusst
  • Ausgenommen davon ist jeder Zustand, der durch den Request vorgegeben ist und sich nicht in dessen Verlauf verändert (z.B.: Produkt, verfügbare Module, Customer, User, Sprache, etc.)
  • Weiterhin ausgenommen ist das Caching des aktuellen Datenbestands

Ein gutes Beispiel wäre ein Logger:

  • Logging enabled: $pDi->get(Logger::class) liefert einen vollfunktionsfähigen Logger
  • Logging disabled: $pDi->get(Logger::class) liefert einen kompatiblen Dummy-Logger

Der DI vereinfacht auch das Unit-Testing. Tests können absolut isoliert laufen, ohne einen Sub-Request starten zu müssen. Jeder Test bekommt einfach einen neuen (vorkonfigurierten) DI-Container.

Nerviges Durchreichen von QueryAdaptern? Nicht mehr nötig, denn das DI-Framework kümmert sich drum.

To be continued …

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert

vier × zwei =