Facebook
Twitter
Google+
Kommentare
25

Inversion of Control

Viele haben sicher schon einmal etwas von Inversion of Control (IoC) und Dependency Injection (DI) gehört. Und sicher auch, dass es ganz toll ist und es zu den best practices gehört. Nur eine Frage die oft nicht wirklich beantwortet wird: Wieso ist das so sinnvoll?

Ich versuche hier einmal diese Frage mit einem Beispiel aus dem realen Leben zu dokumentieren und zu beantworten…

Wir stellen uns eine größere mittelständige Firma vor. Sie hat mehrere  Abteilungen die eigenständig arbeiten. Natürlich müssen da auch Dinge angeschafft werden und das kostet Geld. Entsprechend ihrem Bedarf bestellen diese Abteilungen also Dinge wie Drucker, Computer oder Spezialhardware. Die Rechnungen werden in die Buchhaltung gegeben und dann von dort aus bezahlt.

Das KANN alles super funktionieren. Aber nur dann, wenn diese Abteilungen auch verantwortungsbewusst mit Geld umgehen und nur Dinge anschaffen, die sie wirklich benötigen. Außerdem ist es schwer einen Abteilungsübergreifenden Überblick zu bekommen, welche Hardware vorhanden ist. Es kann auch passieren, dass mehrere Abteilungen die selbe Spezialhardware bestellen obwohl sie sich diese eigentlich hätten teilen können. Auch ist es schwer nachzuvollziehen wo denn das ganze Geld geblieben ist.

Wie kann man diesen Zustand also optimieren? So wie es in der Praxis oft gehandhabt wird: Die Abteilungen melden am Anfang eines Jahres ihren Bedarf an, daraus wird ein Gesamtbedarf ermittelt und ein Jahresbudget errechnet. Das wird dann angepasst und optimiert und jeder Abteilung ein Budget, welches sie zur Verfügung haben, mitgeteilt. Größere Anschaffungen müssen jedoch erst genehmigt werden (damit kann man doppelte Einkäufe teurer Spezialhardware vermeiden). Und da diese größeren Anschaffungen nur

noch von einer zentralen Stelle vorgenommen werden hat man darüber auch immer einen Überblick.

Und Dinge die sich so im Alltag bewährt haben machen auch beim Softwaredesign Sinn. Wieso soll sich eine Klasse Gedanken machen woher sie ihren Logger bekommt? Oder wo sie einen Zugang zur Datenbank findet? Das ist gar nicht ihre Aufgabe. Es ist viel sinnvoller einer Klasse einfach einen Logger oder eine Datenbankverbindung an die Hand zu geben und sie macht das was sie am besten kann. Ihre eigene Logik zu implementieren.

Soweit die graue Theorie. Ich hoffe ich habe nun alle (oder zumindest viele) von dem Nutzen der IOC und DI überzeugen können. Denn nun möchte ich zeigen, wie die Umsetzung dessen in der Praxis aussehen kann.

Hier der bisher unschöne Weg wie eine Klasse Ressourcen verwendet:

 class MyClass { private $_logger; private $_dbHandle; 
 function __constructor() { $this->_logger = Zend_Registry::get('LOGGER'); $this->_dbHandle = Zend_Registry::get('DB'); } function doSomething() { // etwas loggen $this->_logger->write('foo');  
 // Daten aus der Datenbank lesen $this->_dbHandle->select('SELECT * FROM bar'); } } 

Die Klasse holt sich im Konstruktor selber ihre Abhängigkeiten und verwendet sie dann.

Besser ist folgender Ansatz:

 class MyClass { private $_logger; private $_dbHandle; function __constructor() { } function setLogger(Zend_Log_Writer_Abstract $logger) { $this->_logger = $logger; } function setDbHandle(Zend_Db_Adapter_Abstract $dbHandle) { $this->_dbHandle = $dbHandle; } function doSomething() { // etwas loggen $this->_logger->write('foo'); // Daten aus der Datenbank lesen $this->_dbHandle->select('SELECT * FROM bar'); } } 

Die Klasse bekommt ihre Abhängigkeiten über Setter-Funktionen gesetzt. Man kann an dieser Stelle die Setter natürlich auch zugunsten von Konstruktor-Parametern ersetzen. Ein offensichtlicher Vorteil ist schon einmal, dass die Klasse sich nicht überlegen muss WOHER sie den Logger oder das DB-Handle bekommt. Der Zugriff über eine Registry ist nichts weiter als eine objektorientierte Form von globalen Variablen. Wenn sich der Zugriffsbezeichner ändert, müssen alle Klassen angepasst werden. Ebenso ist der Rückgabewert einer solchen Registry nicht Typsicher. Dieses Problem haben wir über die Setter-Funktionen gelöst.

Soweit möchte ich hier erst einmal Schluss machen für diesen Artikel. Im nächsten Artikel möchte ich dann einmal zeigen, wie man sehr einfach diese Dependency Injection umsetzen kann.

Über den Autor

Sven Weingartner

Sven Weingartner ist aktuell Software-Entwickler bei einem großen deutschen Portalbetreiber. Nach einem Ausflug in die Enterprise-Entwicklung mit Java und SAP ist er seit drei Jahren wieder in der professionellen PHP-Entwicklung tätig. Dabei hat er vor allem Portale mit sehr hohen Besucherzahlen und weltweiter Internationalisierung entwickelt. Daher liegt sein Schwerpunkt vor allem in Performanceoptimierung und der Systemarchitektur.
Kommentare

25 Comments

  1. Abgesehen von den bereits beschriebenen Vorteilen wird auch das Testen einfacher werden, wenn die Abhängigkeiten nicht fest verankert sind und später gegen Dummies getauscht werden können.

    Reply
  2. Die Klasse bekommt ihre Abhängigkeiten über Setter-Funktionen gesetzt. Man kann an dieser Stelle die Setter natürlich auch zugunsten von Konstruktor-Parametern ersetzen.

    Ist sowas damit gemeint:

    public function __construct(vB_Registry $vbulletin, vB_Database $osdb = null)
    {
    $this->registry = $vbulletin;
    if ($osdb == null)
    {
    $this->osdb = $this->registry->db;
    }
    else
    {
    $this->osdb = $osdb;
    }
    }

    Reply
  3. @ragtek Ja so in der Art. Ich weiß jetzt nicht genau was die vB_Registry macht, aber an sich soll auf eine Registry möglichst komplett verzichtet werden. Weil in deinem Beispiel holt er sich ja wieder das DB-Handle aus einer Registry, was ja vermieden werden soll. Aber Grundsätzlich ist der Ansatz so wie du ihn gepostet hast. Für Mein Beispiel sähe das so aus:

    class MyClass
    {
    private $_logger;
    private $_dbHandle;

    function __constructor(Zend_Log_Writer_Abstract $logger,
    Zend_Db_Adapter_Abstract $dbHandle)
    {
    $this->_logger = $logger;
    $this->_dbHandle = $dbHandle;
    }

    function doSomething()
    {
    // etwas loggen
    $this->_logger->write(‚foo‘);

    // Daten aus der Datenbank lesen
    $this->_dbHandle->select(‚SELECT * FROM bar‘);
    }
    }

    Reply
  4. Hi,

    Nachteile der gezeigten Methode (via Setter statt Parameter im Constructor):

    – Das Objekt ist nicht direkt nutzbar nach Aufruf des Constructors, sondern Setter müssen noch aufgerufen werden.

    – Ich muss in der Doku nachschauen, wie ich das Objekt korrekt initialisiere. Das sagt mir auch keine Code-Completion einer IDE.

    Da gibt es verschiedene Möglichkeiten (deren Vor- und Nachteile ich hier nicht ausführe):

    – Dem Constructor müssen die Objekte mitgegeben werden.

    – Der Constructor erzeugt Standard-Objekte (für Logger & Co); diese können mit Settern verändert werden.

    – Man arbeitet nicht mit $this->logger, sondern mit $this->getLogger() und falls der User keinen Logger mit einem Setter gesetzt hat, wird ein Standard-Objekt erzeugt.

    Schöne Grüße

    Thomas

    Reply
  5. Bin ich noch nicht glücklich mit. Wenn ich Abhängigkeiten manuell setze, dann habe ich doch ebenfalls an zig Stellen im Code fest eingebaut was benutzt werden soll, z.B. welcher Logger. Wie wechsele ich denn diese nun aus ohne alle diese Code-Stellen zu ändern?

    Eine Registry hätte ja wenigstens den Vorteil, dass ich das jeweils verwendete Objekt an einer zentralen Stelle ändern kann.

    Reply
  6. @Thomas
    Objekte sollen auch nicht nur über den Konstruktur erzeugt werden (also kein $myObj = new MyClass()), sondern über den IOC-Container geholt/ erzeugt werden. Also $myObj = IocContainer::getClass(‚MyClass‘). Der IOC-Container muss natürlich korrekt konfiguriert sein, damit alle Klassen auch sauber erzeugt werden.

    Wobei man in der Tat mit einer Übergabe der Werte im Konstruktor eine bessere Dokumentation im Code hat und auch im Fehlerfall schneller erkennen kann wo die Ursache liegt.

    Reply
  7. @sven naja bei mir ist es etwas schwierig.

    Die Klasse holt Artikel aus dem Shop und gibt diese aus.

    Da ich nicht weiß, wie die DB beim Benutzer aufgebaut ist (es ist möglich das die Shoptabellen in der selben DB wie das vBulletin sind, aber es ist genauso möglich dass es sich um 2 verschiedene Datenbanken handelt, dann brauche ich eine 2. Verbindung) brauche ich das.
    Falls eine 2. Verbindung übergeben wird, benutze ich diese, ansonsten die „vB eigene“ aus der Registry.

    Reply
  8. Ich will auch mal ein paar kritische Worte zum IoC / DI – Pattern loswerden. Im Prinzip wird das Pattern doch nur so massiv verwendet, weil es die Testbarkeit von Code so erhöht (was natürlich grundsätzlich gut ist). Ansonsten hat das Pattern ohne Anwendung eines DOC (Dependency Injection Container) auch durchaus Nachteile. Will ich meinen Logger bei DC austauschen, musst ich dies bei jeder Objekt-Instanzierung tun, wenn ich es hart verdrahtet im Constructor habe, habe ich nur eine Stelle zum Anpassen (DRY-Principle).

    @Christian
    Zend_Registry ist nichts anderes als ein globaler Container. Früher hat man dafür noch globale Variablen verwendet, heutzutage ist man mehr OO & cooler und nimmt eine registry. Bleibt aber das gleiche! 😉 (Was im übrigen gut ist, eine Registry erleichert das Programmieren enorm, da es global vorgehalten werden kann, bei einem exzessiven Gebrauch sollte man aber auch überlegen ob man software architektonisch alles richtig macht).

    @Sven
    Danke, schöner Beitrag! Würde gerne vor allem mehr noch dazu lesen, wo denn der Unterschied zwischen IoC und DI liegt, denn DI ist doch nur eine Art von IoC. Was sind denn andere Möglichkeiten?

    Reply
  9. @jonas
    Wie man IOC in der Praxis einsetzen kann darauf werde ich in meinem nächsten Artikel eingehen. Erst einmal wollte ich die Grundlagen zeigen also wozu das ganze eigentlich sinnvoll ist. Da wirst du sehen, dass man diese Dinge eben nur an einer Stelle zentral definiert und konfiguriert.

    Reply
  10. Wenn sich der Bezeichner ändert, muss doch immer noch Code an verschiedenen Stellen angepasst werden (beim Setter jeder Klasse). Nur sind es eben andere Stellen. Ich sehe den Vorteil in dieser Hinsicht nicht.

    Reply
  11. Die Verbesserung der Testbarkeit ist das am häufigsten genannte Kriterium für den Einsatz von DI. Ich bin überzeugt davon dass es aber noch weitere Vorteile bringt, das zeigt mir zumindest meine Erfahrung aus den letzten 4 Jahren. Durch DI wird der Programmcode viel lesbarer und somit übersichtlicher da man sich quasi nur noch mit der Business-Logik befasst. Die Konfiguration wird nach extern ausgelagert, unabhängig davon ob dies in PHP, XML, YAML oder JSON erfolgt.

    Je nach Anwendungsfall lässt sich eine reale Klasse via Konfiguration in verschd. Ausprägungen verwenden ohne dass der Entwickler dabei mehr Programmcode schreibt. Auf der anderen Seite lässt sich beispielweise bei der Entwicklung die Schnittstelle zu einem externen Webservice gegen eine Dummy-Implementierung austauschen und somit die Responses beeinflussen. Programmcode wird dabei nicht verändert, nur die Konfiguration. Klingt banal, ist es auch 🙂

    Das ganze setzt aber voraus dass man ein Framework verwendet welches auf DI getrimmt wurde. Den Einsatz eines einfachen DI Containers würde ich nicht empfehlen man damit die Anwendung wieder abhängig von einer externen Komponente (dem DI Container) macht.

    Reply
  12. @Ulf: Das Beispiel mit dem Logger ist nicht ganz korrekt. Du musst dich nicht darum kümmern die Abhängigkeiten an den Consumer weiterzugeben. Das wird vom DI Container erledigt. Du musst einzig beim Container nach der passenden Instanz fragen und bekommst diese dann fertig konfiguriert geliefert. Dies dürfte meiner Meinung nach auch der große Unterschied zwischen einer Registry und einem DI Container zu sein. Die Registry ist dumm und liefert nur das was du reinsteckst zurück. der DI Container hingegen instanziiert die Objekte und deren Abhängigkeiten eigenständig und weiß wann und wie er eine Instanz zurückgeben muss (aufgrund seiner Konfiguration).

    Reply
  13. Was haltet ihr denn in diesem Zusammenhang von Signalen und Slots ?

    Ich finde darüber könnte man auch mal reden.

    Reply
  14. @Stefan
    Klar, bei Verwendung eines DI-Containers hast du die Code-Stelle auch wieder gekapselt, ich bin aber bei meinem Beispiel davon ausgegangen das dieser nicht vorhanden ist. Das sehe ich selbst so öfters (und mache es auch selber so 😉 ), da es im PHP-Bereich einfach einen Mangel an guten DI-Containern gibt. Bei Spring ist das z.B. eine Wohltat, da kann man nicht viel falsch machen (bzw. wenn man etwas falsch macht, dann funktioniert es eh nicht).

    Reply
  15. Guter Artikel, auch wenn nur Grundzüge angerissen werden.

    Ich möchte allerdings noch einmal darauf hinweisen, dass Setter-Injection aus meiner Sicht kein guter Weg ist.

    In Java würde man sich früher oder später mit NullPointer-Exceptions konfrontiert sehen.

    Besser im Konstruktor und wissen, dass alles nötige initialisiert wurde!

    Reply
  16. @Julian
    Das Argument ist bei PHP nicht so relevant, da man durch entsprechende TypeHins bei den Setter-Parameter ein Null-Value ausschließen kann. Im Gegensatz zu Java ist nämlich null kein WhatSoEverObject.

    Trotzdem würde ich gründsätzlich auch eher Construktur-Injection bevorzugen. Ein guter DI-Container kann diese Funktionalität aber sicherlich auch gut mit settern und der magischen __set Methode umsetzen. Zwar dann wieder schwierig zu debuggen und nachzuvollziehen, aber fancy… 😉

    Reply
  17. > da man durch entsprechende TypeHins bei den Setter-Parameter ein Null-Value ausschließen kann.

    nicht, wenn man die Setter vergisst aufzurufen 😉

    Reply
  18. Ich versuche ihn diese Woche zu schreiben. Darin möchte ich einerseits zeigen wie man selber einen simplen IoC Container programmiert (einfach um die Hintergründe zu verstehen) und dann auch noch einen kurzen Überblick über bestehende IoC-Container auf dem Markt.
    Sollte dann also in den nächsten 1-2 Wochen hier erscheinen.

    Reply

Leave a Comment.

Link erfolgreich vorgeschlagen.

Vielen Dank, dass du einen Link vorgeschlagen hast. Wir werden ihn sobald wie möglich prüfen. Schließen