Facebook
Twitter
Google+
Kommentare
9

Inversion of Control – Teil 2

In meinem ersten Artikel habe ich gezeigt was Inversion of Control (IoC) und Dependency Injection (DI) sind und warum sie so gut sind.

Nachdem ich so viel positive und konstruktive Kritik bekommen habe, habe ich diese natürlich auch in diesen Artikel einfließen lassen. Ich werde nun also am Anfang noch einmal kurz den Unterschied zwischen IoC und DI erklären. Danach werde ich anhand einer einfachen Beispiel-Implementierung zeigen wie DI implementiert werden kann. Im dritten Teil werde ich dann auf die DI von Symfony 2.0 eingehen.

Unterschied zwischen Inversion of Control (IoC) und Dependency Injection (DI)

Bei der Unterscheidung halte ich mich an Martin Fowler.

Wenn man Inversion of Control einsetzt nennt man diese lose gekoppelten Klassen Komponenten und Services. Komponenten sind dabei eher „dummy“ Objekte, die Logik beinhalten aber nicht direkt mit anderen Systemen kommunizieren. Deren Aufgabe ist also hauptsächlich die Berechnung von Daten oder Kapselung von Logik. Services interagieren fleissig mit anderen Services oder Komponenten. Inversion of Control bezeichnet dabei einfach nur, dass die Abhängigkeiten nicht fest einprogrammiert sind, sondern von außen eingeimpft werden. IoC beschreibt also einfach das grundsätzliche Paradigma.

Dependency Injection hingegen beschreibt wie diese Abhängigkeiten gesetzt werden. Beim letzten Mal habe ich bereits auf die Möglichkeit hingewiesen, dass diese Abhängigkeiten über Konstruktoren gesetzt werden können oder aber auch über Setter-Methoden.

In einer Präsentation von Fabien Potencier wird noch die Property-Injection genannt. Dies ist ganz einfach

$myClass = new MyClass();
$myClass->logger = $logger;

Kann man machen muss man aber nicht. Vor allem sollte man das gar nicht. Öffentliche Eigenschaften sind im Sinne der OO sehr unschön und führen zu vielen Seiteneffekten. Also vergessen wir das auch mal ganz schnell wieder.

Eine weitere von Fowler gezeigte Möglichkeit ist die Interface Injection. Hier mal ein kleines Beispiel

interface MyClassInterface {
    function setLogger(Zend_Log_Writer_Abstract $logger);
}

Es soll hierbei mal egal sein ob Zend gut oder schlecht oder besser als etwas anderes ist. Zend_Log_Writer_Abstract ist einfach eine Abstrakte Oberklasse von der es noch weitere konkrete Implementierungen gibt. Da MyClass dieses Interface implementiert bekommt sie automatisch die Abhängigkeit zu diesem Logger gesetzt. MyClass kommt also gar nicht in die Verlegenheit sich selber einen Logger zu suchen.

Interface Injection ist nicht ganz trivial und man muss es sich eine Weile durch den Kopf gehen lassen. Während die anderen Typen der DI eher konkrete Umsetzungen definieren, so definiert Interface Injection eher ein Pattern.

Beispielimplementierung eines Containers zur Verwendung eigener Dependency Injection

Da wir nun wissen, dass es besser ist, dass Abhängigkeiten von außen gesetzt werden wollen wir einmal schauen wie das am besten geht.

Ohne IoC haben wir einfach folgendes gemacht:

$myClass = new MyClass();

MyClass hat sich dann die Datenbankverbindung und den Logger selber geholt. Mit IoC müssten wir so etwas schreiben:

$dbHandle = new Zend_Db_Adapter_Pdo_Mysql(array(
    'host'     => '127.0.0.1',
    'username' => 'webuser',
    'password' => 'xxxxxxxx',
    'dbname'   => 'test'
));
$logger = new Zend_Log();
$myClass = new MyClass();

$myClass->setDbHandle($dbHandle);
$myClass->setLogger($logger);

Und das an allen Stellen im Code wo wir MyClass verwenden wollen. Nicht sehr schön und so bringt das auch keine Vorteile. Viel schöner wäre es, wenn es eine Instanz geben würde, die weiß wie sie all diese Komponenten zusammen bauen muss. Wo ich einfach nur noch sage: Gib mir eine frische MyClass.

Na dann fangen wir an…

Das Grundkonzept ist, dass man eine Art Registry hat wo man sich seine Objekte abholt. Konkret wird diese Registry als Container bezeichnet.

Zuerst definieren wir einmal wie unsere Applikation aussehen soll:

$classDefinition = array(
    'Action_Admin_Login' => array(),
    'Action_Game_Login' => array(),

    'Controller_Model_Admin' => array(),
    'Controller_Rpc' => array(),

    'View_Admin_Login' => array(),
    'View_Admin_Main' => array(
        'Controller_Main'),
    'View_Admin_UserGroupList' => array(
        'Controller_UserGroupList'),
    'View_Admin_UserGroupListModify' => array(
        'Controller_UserGroupList'),
    'View_Admin_UserList' => array(
        'Controller_UserList'),
    'View_Admin_UserShow' => array(
        'Controller_UserShow'),

    'View_Game_Main' => array()
);

Dies ist ein einfaches Array, wo jede Klasse die wir konfigurieren als Schlüssel definiert wird. Als Wert hinterlegen wir ein Array mit Namen von Objekten die wir in der Klasse setzen wollen. Dies wird ein sehr einfacher Container, deshalb verwenden wir hier Setter-Injection. Ebenso können unsere Klassen nicht mit Parametern konfiguriert werden. Aber es geht hier auch nicht um einen Container für den Produktiveinsatz sondern um das grundlegende Konzept zu demonstrieren.

Wir haben nun also unsere Registry, welche noch recht leer ist:

class Registry
{

    public static $classDefinition = array();

    public static function getInstance ($className)
    {
         // @todo
    }

}

Dann füllen wir mal die getInstance mit Leben…

        array_push(self::$stack, $className);

Wir legen den Klassennamen auf den Stack. Dies ist notwendig da wir ja eventuell komplexere Bäume laden. Ebenso bauen wir einen Hash auf, welche Klassen wir gerade instanziieren um Schleifen zu erkennen und abzubrechen. Wenn wir also eine Schleife erkennen brechen wir ab.

        if (isset(self::$hashStack[$className])) {
            die('Loop in Objektinstanziierung.');
        }
        self::$hashStack[$className] = true;

Damit ein Objekt nicht jedesmal neu erzeugt werden muss cachen wir es in der Klassenvariablen $object. Wenn das Objekt also noch nicht existiert erzeugen wir es.

Dabei laden wir alle Klassen die wir brauchen. Diese laden wir rekursiv über getInstance(). Wir gehen davon aus, dass für alle Objekte entsprechende Setter- und Getter-Methoden existieren und setzen das Objekt darüber. Am Ende cachen wir es lokal.

        if (! isset(self::$objects[$className])) {

            $injection = self::getClassDefinition($className);
            if (is_array($injection)) {
                $obj = new $className();

                foreach ($injection as $tempClassName) {
                    $setterFunction =
self::getSetterFunctionForClassName($tempClassName);
                    $tempObj = self::getInstance($tempClassName);
                    $obj->$setterFunction($tempObj);
                }

                self::$objects[$className] = $obj;
            }
        }

Nun noch die Stacks aufräumen und das fertige Objekt zurück geben.

        unset(self::$hashStack[$className]);
        array_pop(self::$stack);

        return self::$objects[$className];

Das war auch schon die Implementierung der getInstance(). Der Vollständigkeithalber hier noch die getSetterFunctionForClassName-Methode. Dabei werden einfach die Unterstriche entfernt und ein „set“ davor gehangen.

    private static function getSetterFunctionForClassName ($className) {
        $functionName = str_replace('_', '', $className);
        return 'set' . $functionName;
    }

Ich hoffe damit das Grundlegende Konzept eines Containers verständlich erklärt zu haben. Im dritten und letzten Teil werde ich dann Dependency Injection im Symfony 2.0 Framework vorstellen.

Ü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

9 Comments

  1. Sorry aber ich sehe da noch ein paar kleinere Probleme, auch wenn der Ansatz sicher möglich und vor allem pragmatisch ist.

    Zum einen kriegst du ein dickes Problem mit dem Caching, wenn du in größeren Dimmensionen arbeitest.
    Da durch den Cache auf alle Objekte gezeigt wird, wird ein Objekt nie mehr vom garbage collector gelöscht, was zu einer immensen Belastung des Arbeitsspeichers bei vielen Objekten führt.
    Sicher nicht immer ein Problem, kann aber eins werden.

    Zum Anderen kann man wieder kritisieren, dass wir „globales“, zentrales Wissen haben. Da es sich letztendlich aber um eine Konfiguration handelt, okay… was passiert wenn wir mehrere Varianten des Konstruktors brauchen?

    Im Grunde alles nur Überlegungen, der Ansatz ist OK, aber ich glaube manches mal fährt man mit einer eigenen Factory, die weniger abstrakt und mächtig ist doch besser.

    Reply
  2. Zu diesem Beispiel:
    $myClass->logger = $logger;

    Ich löse sowas gerne über die magischen Methoden __set und __get, denn ein Setter ist nichts anderes als eine Wertzuweisung und diese Variante daher eigentlich sogar schöner.

    Vergleiche:
    $foo=$bar;
    $foo[$index]=$bar;
    $foo->setBar($value);

    Mit:
    $foo=$bar;
    $foo[$index]=$bar;
    $foo->bar=$value;

    Da zeigt doch die zweite Variante eindeutig mehr Einheitlichkeit.

    Reply
  3. @Timo
    Wenn die Variablen alle private oder protected sind und die Magische Methode prüft, dass keine neuen Variablen angelegt werden, dass ist dagegen auch nichts einzuwenden.
    Wobei dann nicht mehr mit einer Checkstyle Regel geprüft werden kann ob auf Variablen direkt zugegriffen wird. Ansonsten kann man schön prüfen dass immer brav get/set/is verwendet wird.

    Reply
  4. @Julian: Ich denke nicht dass die vielen Objekte ein Problem darstellen da i.a.R. nur die Objekte instantiiert werden die (für den aktuellen Request) benötigt werden. Wir nutzen DI schon lange und das auch in komplexen Projekten, Speicherprobleme aufgrund großer Objektbäume sind mir keine bewusst.

    Der Punkt globales, zentrales Wissen stimmt natürlich. Das Problem m.E. ist dass in solchen Beispielen immer recht einfache Container Verwendung finden. In der Praxis sollte man eher das einsetzen was ich gerne als Full-Stack-Framework bezeichne. Also ein Framework was komplett auf DI getrimmt ist. In dem Fall entkoppelst du die Anwendungs-Logik komplett vom darunter liegenden Framework. Natürlich steckt das Konfigurationswissen immer noch in einem zentralen Objekt. Allerdings bekommt deine Anwendung davon nichts mit weil dein Anwendungscode nicht nach den Dependencies frägt sondern diese automatisch vom Framework zum Zeitpunkt der Instanziierung geliefert bekommt (Don`t call us, we call you).

    Die Verwendung einer Factory ist streng genommen suboptimal wenn du die Anwendung testbar gestalten möchtest. Factories erzeugen i.a.R. eine direkte Abhängigkeit zu einer konkreten Implementierung die du in deinem Test-Enviroment nicht austauschen kannst. Natürlich gibt es dafür wieder work-a-rounds, aber sauber ist das nicht 🙂

    Reply
  5. @Stephan: Wir hatten solche Fälle schon bei sehr aufwändigen Berechnungen, wie gesagt, es kommt nicht häufig vor aber die Objekt-Caches können da wirklich zu Problemen führen. Man beachte auch, dass der Cache dabei ja je Prozess aufgebaut wird, die Anzahl der Benutzer dabei also die Auslastung stark steigen lässt.
    Keine generelle Kritik, Caches sind OK, aber einen Hinweis ist es wert.

    Reply
  6. Hmm man könnte dies Lösen indem man dem Container einen eigenen kleinen Garbage Collector mitgibt. Dieser prüft ob es außer ihm noch andere Referenzen auf die Objekte gibt… Dafür könnte man debug-zval-dump verwenden.
    Wobei allerdings die zyklische Referenz innerhalb der Objekte der Fallstrick sein dürfte.
    In Java gibt es für sowas so etwas wie „Soft-Links“. Damit hindert man den gc dann nicht am Aufräumen wenn man etwas cached.

    Reply
  7. Hi Sven, der Ansatz ist nicht schlecht.
    Allerdings wenn ich ein Objekt öfter benötige, arbeite ich mit Singletons und Late Static Bindings. Ich habe gehört das schöne Arbeiten damit lässt bisher einzig und allein PHP zu. Das Prinzip geht dahin, dass bei getInstance() das Objekt sich in die priv. stat. Variable instanziert (sofern nicht schon beim vorherigen Aufruf passiert) und nur die Referenz darauf zurück gibt.
    Der Erstaufruf mit DI passiert dann beim Erstaufruf in der Bootstrap – wo die App-Config eh geladen wird.

    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