Dependency Injection für Projektentwickler in OXID

Teil 1: Die Grundlagen

Es gibt eine ganze Reihe von Techniken und Heuristiken um test- und wartbare Software zu schreiben. Doch letztendlich laufen alle auf eine Grundregel hinaus: Eine so weit wie möglich gehende Entkopplung aller Softwarebausteine.

Die Logik dahinter ist einfach: Wenn die einzelnen Bausteine autark sind, können sie auch isoliert getestet werden. Und damit kann, wenn die Tests vernünftig geschrieben sind, für jeden einzelnen Baustein sichergestellt werden, dass er die an ihn gestellten Anforderungen erfüllt.

Aber natürlich müssen diese einzelnen Bausteine irgendwann zu einem Ganzen integriert werden. Um hier nicht wieder neue Abhängigkeiten zu schaffen und die mühsam erreichte Entkopplung der einzelnen Bausteine zu verlieren, greift man zur Technik der inversion of control. Diese wird auch Hollywood-Prinzip genannt, gemäß der Standardansage: "Don't call us, we call you". Übersetzt in die Welt der Softwareentwicklung: Wenn einer unserer Softwarebausteine die Funktionalität eines anderen Bausteins benötigt, dann darf er diesen Baustein nicht selbst instantiieren. Vielmehr bekommt er ihn von außen injiziert.

Um zu verstehen, warum das essentiell ist, nehmen wir ein ganz einfaches Beispiel:


class BausteinI {

  public function __construct()
  {
    $this->b2 = new BausteinII();
  }

  public function someFunction(int $param): int
  {
    $input = $param * $this->getFactor();
    return $this->b2->doSomething($input);
  }

  private function getFactor(): int
  {
    return 12;
  }
}

Wenn wir jetzt die Methode someFunction() testen wollen, dann müssen wir wissen, was genau die Klasse BausteinII tut, um das Ergebnis zu testen, denn BausteinII ist fest verdrahtet mit der Klasse, die wir hier testen wollen. Was sollen wir also im folgenden Testcode an die Stelle von ? setzen? Ohne Kenntnis von BausteinII wissen wir das nicht.

 


class BausteinITest {

  public function testSomeFunction()
  {
    $bausteinI = new BausteinI();
    $this->assertEquals(?, $bausteinI->someFunction(5));
  }
}

Und selbst wenn wir einen Wert ermittelt haben, kann uns das trotzdem Probleme machen. Was ist, wenn BausteinII unterschiedliche Ergebnisse zurückliefert, beispielsweise in Abhängigkeit von der Mondphase (oder realistischer: der Zeitzone oder eine Konfigurationsoption)? Dann kann sich der Rückgabewert ändern und unser Test schlägt in manchen Fällen fehl, in manchen nicht.

 

Das sind alles Fragen, die wir uns überhaupt nicht stellen wollen, wenn wir BausteinI testen, denn eigentlich wollen wir uns nicht damit auseinandersetzen, was BausteinII tut. Das ist die Aufgabe derjenigen, die diese Komponente entwickeln.

 

Deshalb ist es besser, die Kontrolle umzukehren. BausteinI soll überhaupt nichts über BausteinII wissen - außer dass letzterer die Methode doSomething() zur Verfügung stellt. Mit anderen Worten: BausteinII sollte ein interface implementieren und nur über dieses ansprechbar sein:

 


interface BausteinIIInterface {
  
  public function doSomething(int $input): int

}

Damit können wir nun den Konstruktor von BausteinI umschreiben und das Abhängigkeitsverhältnis umkehren:

 


public function __construct(BausteinIIInterface $b2)
{
  $this->b2 = $b2;
}

Jetzt können wir für BausteinI einen Test schreiben, ohne uns im Geringsten um BausteinII irgendeinen Gedanken zu machen:

 


public function testSomeFunction()
{
  $bausteinIIMock = $this->createMock(BausteinIIInterface::class);
  $bausteinIIMock->expects($this->once())
    ->method('doSomething')
    ->with(60);

  $bausteinI = new BausteinI($bausteinIIMock);
  $bausteinI->someFunction(5);
}

Wir testen jetzt nur noch, dass der korrekte Parameter an BausteinII übergeben wird, also das, was wir in BausteinI als eine Art Geschäftslogik implementiert haben. Was BausteinII damit macht, ist uns völlig egal, weswegen wir ihn durch ein mock ersetzt haben, das einen Fehler produziert, wenn im Verlauf des Tests nicht genau einmal die Methode doSomething() mit dem Parameter 60 aufgerufen wird.

Und damit haben wir einen echten unit test für unseren BausteinI geschrieben, der die Geschäftslogik von BausteinII völlig außen vor lässt (okay, wenn man der ganz reinen Lehre folgt, auch nicht ganz, weil wir immer noch Kenntnis von Implementationsdetails voraussetzen, nämlich eben diesen Aufruf von BausteinII; aber in der Praxis ist das nicht immer zu vermeiden).

 

Um also wart- und testbaren Code zu schreiben, ist das Prinzip der inversion of control unverzichtbar. Und wie unser voll ausgeführtes Beispiel zeigt, ist für die Implementierung dieses Prinzips auch kein framework oder ähnliches nötig. Inversion of control gibt uns eine Technik an die Hand, wie wir Abhängigkeiten innerhalb unseres Codes aufheben können.

 

 

Zur Laufzeit des Programmes müssen natürlich die einzelnen Bausteine konkret verdrahtet werden. Das kann man, zumindest bei Kleinprojekten, problemlos selbst im Code machen.

Doch ab einer gewissen Komplexität ist es sinnvoller, ein framework zu verwenden, das die einzelnen Komponenten zusammenstöpselt. Dabei sollte es im Prinzip egal sein, welches framework man verwendet: Der Code sollte sich dadurch nicht ändern.

OXID hat sich dazu entschieden, den Symfony DI container in sein E-Commerce framework zu integrieren. Die Gründe dafür waren:

  • Es handelt sich um ein etabliertes framework, das kontinuierlich gewartet wird.
  • Der container bietet ein sehr gutes caching und damit eine gute performance.
  • Er unterstützt zusätzliche nützliche Funktionalitäten, vor allem events.

 

Bei seiner Einführung in der Version 6.0 war der Symfony DI container nur innerhalb eines speziellen namespaces innerhalb des OXID frameworks nutzbar und damit für Projektentwickler eigentlich uninteressant. Jetzt haben wir die Nutzung des DI containers auch für Modul- und Projektentwickler freigegeben. Die folgenden Blog-Beiträge werden darauf eingehen, wie der Symfony DI container in Modulen und für die Projektentwicklung genutzt werden kann (und sollte).

 

Dies ist der erste Artikel einer dreiteiligen Serie. Im nächsten Teil wird es darum gehen, wie man innerhalb eines OXID Moduls die Funktionalität des DI containers nutzen kann.

Und im dritten Teil dann, welche Möglichkeiten der DI container bietet, um Shop-Funktionalitäten zu erweitern oder zu ersetzen, ohne auf oxNew() zurückgreifen zu müssen.

Tel. +49 761 36889 0
Mail. [email protected]