Artikelformat

Dependency Injection (Teil 1)

Das Grundkonzept der Inversion of Control wird aktuell sehr oft durch eine Dependency Injection Implementierung erfüllt. HIerbei wird zumeist auf DI-Container zurückgegriffen. Es ist aber vom Konzept nicht unbedingt notwendig und daher gibt es im Teil 1 der Artikelserie ein Beispiel für eine Art Dependency Injection zu nutzen ohne auf einen DI-Container zurück zugreifen.

Zuerst muss man das Problem klar machen, das Dependency Injection lösen möchte. Man sollte ein Konzept oder Pattern nicht benutzen weil es gerade cool ist, sondern weil ein Problem besteht. Zumindest ist diese Vorgehensweise mE sinnvoll und führt nicht zu einem Pattern-Zoo.

Angenommen man hat eine Klasse und in dieser wird auf eine DB zugegriffen. So möchte man nicht jedem Aufruf der Klasse eine neue Datenbankverbindung öffnen, sondern auf eine bestehende Verbindung zurückgreifen. Ein ganz beliebtes Pattern hierfür ist das Singleton. Leider ist man hier in einem Testfall ziemlich aufgeschmissen, da man eine DB benötigt, um den Unittest ausführen zu können. Man möchte lieber ein Mock-Objekt nutzen. Nur wie kann man das Datenbank-Objekt durch ein Mock-Objekt ersetzen. Und genau hier kommt die Dependency Injection ins Spiel. Das Datenbank-Objekt ist eine Dependency und diese stecken wir nun irgendwie in die Beispielklasse.

class Example {

     public function getSomething() {
          return secretFunction(Database::get()->query("Select * from test"));
     }

}

Constructor Injection:
Die erste Idee ist die Konstruktor Injection. Dabei erweitert man den Konstruktor der Beispielklasse um ein Parameter, über das wir dann ein Datenbankobjekt in die Beispielklasse stecken können. Die Beispielklasse kann dann auf einem Klassenattribut arbeiten. Im Unittest funktioniert das Spiel genauso, jedoch wird die Beispielklasse dann mit einem Mock-Objekt initialisiert. Somit haben wir die beiden Klassen entkoppelt und die Testbarkeit des Gesamtcodes erhöht.

class Example {

     private $db;

     public function __construct(MyDatabase $db) {
          $this->db = $db;
     }

     public function getSomething() {
          return secretFunction($this->db->query("Select * from test"));
     }

}

Nachteile:
Ein Problem bei der Constructor Injection sind Klassen, die ein paar mehr Abhängigkeiten haben. Dann wird der Aufruf des Konstruktors aufgebläht und schnell unübersichtlich. Weiter muss die Datenbank immer weitergereicht werden. Sodass teilweise Klassen die Datenbank kennen, obwohl sie diese eigentlich gar nicht benötigen. Wenn die Beispielklasse aber initialisiert werden soll, muss die Datenbank eben vorhanden sein.

12 Kommentare

  1. Bei den Nachteilen stimme ich nicht überein – eine Klasse mit vielen Abhängigkeiten hat diese auch ohne DI. Es fällt dann aber wenigstens auf. Eventuell ist sie schon zu komplex und sollte aufgespalten werden.

    Dass eine Klasse Objekte kennt, die sie gar nicht braucht, ist ebenfalls kein Nachteil von DI, sondern eher ein Designfehler der Klasse(n). Da wäre aber ein konkretes Beispiel hilfreich. 😉

    Antworten
  2. Kommt mir irgendwie bekannt vor. Kommt als nächstes die Injection über Init()? 🙂

    Wie auch immer, das Beispiel ist schlecht gewählt. Die Datenbank könnte hier ja einfach an Example::getSomething() übergeben werden. Von daher scheint mir das DI-Prinzip noch nicht ganz verinnerlicht.

    Antworten
  3. Warum keine Statische Funktionen? DB Klassen werden doch immer und überall gebraucht?

    return secretFunction(DbKlasse::model()->query(…));

    Antworten
  4. Weil die nicht austauschbar sind. Z.B. beim Testen, Debuggen, Umstellen auf eine neue Datenschicht.. Nochmal an den Code „ranzumüssen“ sollte schlicht vermieden werden.

    Antworten
  5. Eine schöne Erklärung allerdings ist das nicht das Dependency Injection Pattern sonder das Inversion of Control Prinzip das besagt das gegen Abhängigkeiten gearbeitet wird. In deinem Beispiel kannst du jetzt ganz einfach deine mysql Datenbank gegen eine sqlite Datenbank oder einer Testdatenbank ersetzten. Wenn man ein kleinen Dependency Injection Container sucht sollte man sich pimple oder twitee anschauen.

    Antworten
  6. @GodsBoss: Klar, die vorhandenen Abhängigkeiten werden dadurch sichtbar. Aber man kann hier auch anders vorgehen und das ist sozusagen schon die Vorbereitung auf Teil 2.
    Wenn man eine Klasse in einer Initialisierungsroutine erstellt (z.B. den Datenbankzugriff) und diese an die Datenzugriffschicht weiterreicht, wandert diese relativ sicher durch Klassen, die keine DB benötigen. Außer man kapselt die Zugriffsschicht wieder und gibt diese direkt weiter. Aber nehmen wir ein übliches Legacy Projekt, da wird dies nicht mal ebenso möglich sein. Es geht aber auch eben anders, wieder eine Vorbereitung 😉

    @Markus: danke für die Tipps. Die Ersetzung würde nur funktionieren, wenn die Konfiguration sauber getrennt ist von der Implementierung. Sollte so sein, ists aber eben nicht immer. Das DB-Singleton kennt man ja aus vielen Implementierungen und darum habe ich es gewählt. Ein Logger wäre auch gegangen.

    Antworten
  7. @Norbert: Keine Ursache. Ich habe mich in letzter Zeit viel mit dem Thema auseinander gesetzt.

    Ja das stimmt wohl, ich hatte vorrausgesetzt das die Konfiguration sauber von der Implementierung getrennt ist. Denn wie heißt es so schön Singleton ist evil.

    Antworten
  8. @Don Zampano
    Nee, da hast Du mich falsch verstanden oder ich habe mich blöd ausgedrückt. Die statische Funktion sollst Du nur zum Zugriff auf eine bestehende Verbindung genutzen werden, also Du erweiterst für Tabelle X Deine DB Klasse und rufst dann X::model()->findByCondition($condition) die Ergebnisse ab.

    Antworten
  9. @Oliver: Alles, was statisch ist, ist Global State und der ist schlecht, weil alles, was vom Global State abhängt, nur noch mit diesem zusammen genutzt werden kann. Ausreichend genutzt, hängt dann im System alles mit allem zusammen, du kannst nichts herauslösen, weder zum Testen noch sonstwie.

    Antworten

Schreibe einen Kommentar

Pflichtfelder sind mit * markiert.