Facebook
Twitter
Google+
Kommentare
26

Dependency Injection Short-Cuts

Diesen Tag widme ich einer Frage, die ich euch stellen möchte. Dabei geht es um Dependency Injection, wir hatten das Thema ja schon mal vorgestellt. Vereinfacht das Testen und ist einfach eine elegante Lösung für manche Probleme. Wir erinnern uns. Also ganz toll das ganze und hurrrraaaa. Ist aber wahrscheinlich das Buzzword des Jahres 2010. Egal machen wir mal ein Dependency Injection Beispiel. Dabei muss man Klassen, die man instanziert immer alle Abhängigkeiten mitgeben. Los gehts:

$logger = new Logger( new FileWriter ( new LinuxWriter ( new ext3( ) ) ) );

Ok ist jetzt ein doofes Beispiel und in der Realität wohl auch eher daneben, aber es veranschaulicht, was passieren kann, wenn man zu viel mit Dependency Injection arbeitet. Der Code wird unübersichtlich. Zumindest muss man eine ganze Menge machen, damit man ein funktionierendes Objekt zur Verfügung hat.Früher hätte das alles die Klasse Logger intern geregelt und man wäre mit new Logger( ) schon ganz schön weit gekommen. Schöne neue Welt? Ich denke schon. Testbarkeit ist halt doch wichtiger als so manch anderes.

Jetzt würde ich mich mal interessieren, wie ihr mir solchen langen Ketten von Injektionen umgeht? Mit der Zeit hat es mich immer genervt, dass man nicht mehr einfach new Logger() nutzen konnte, deswegen habe ich mir das selber erstellt. Und zwar so:

Class Logger
{
  public static getStandardLogger( )
  {
    return new self( new FileWriter ( new LinuxWriter ( new ext3( ) ) ) );
  }
}

Jetzt kann ich wenn ich das Zusammenspiel der Klassen testen will noch alles per Dependency Injection reingeben und trotzdem habe ich einen angenehmen mir eine Standardinstanz zu besorgen. Leider hatte ich noch nicht die Gelegenheit mit den ganzen DI-Implementationen der großen rumzuspielen. So könnte mir stubbles bestimmt eine Lösung bieten, vielleicht aber auch die tollen Zeilen Code von Fabien Potencier.Naja ich merke ich sollte mehr zu dem Thema lesen, dann kann ich auch bessere Beispiele anbringen.

Jetzt aber nochmal zurück. Wie löst ihr das „Problem“ mit den Injektions-Ketten?

Über den Autor

Nils Langner

Nils Langner ist der Gründer von "the web hates me" und auch der Hauptautor. Im wahren Leben leitet er das Qualitätsmanagementteam im Gruner+Jahr-Digitalbereich und ist somit für Seiten wie stern.de, eltern.de und gala.de aus Qualitätssicht verantwortlich. Nils schreibt seit den Anfängen von phphatesme, welches er ebenfalls gegründet hat, nicht nur für diverse Blogs, sondern auch für Fachmagazine, wie das PHP Magazin, die t3n, die c't oder die iX. Nebenbei ist er noch ein gern gesehener Sprecher auf Konferenzen. Herr Langner schreibt die Texte über sich gerne in der dritten Form.
Kommentare

26 Comments

  1. Ich bevorzuge immer noch die gute alte Getter-/Setter-Strategie. Der Getter wird intern genutzt um das Objekt zu holen. Ist noch kein Objekt gesetzt, implementiere ich hier die Initialisierung des gewünschten Standard-Objekts. Mittels des Setters kann jederzeit von außen eine beliebig andere Abhängigkeit injiziert werden.

    Irgendwie versteh ich den ganzen Wirbel um dieses Pattern nicht ganz. Für mich wirkt es nicht besser als die Setter-Variante. Ganz m Gegenteil. Wenn du nun eine Komponente umbaust und plötzlich eine neue Abhängigkeit hast, musst du alle aufrufenden Objekte um den zusätzlichen Parameter erweitern.

    Gruß, Micha

    Reply
  2. Hallo Nils,

    ich selbst schreibe mir in der Regel Helper-Klassen, mittels denen ich mir die entsprechenden Objekte erzeugen lasse. Diese Helper können sowohl projekt-spezifische Parameter abhandeln (wie in deinem Beispiel die Art des FileWriters) oder auch die Erzeugung komplexerer Objekt-Strukturen vereinfachen.

    Das hast du ja mit deinem Logger::getStandardLogger() im Ansatz ähnlich umgesetzt. Alternativ kannst du im Logger prüfen, ob ein FileWriter übergeben wurde. Wenn der Wert null ist, erzeugst du (im Konstruktor, oder mittels Lazy Loading beim ersten Aufruf, wo es intern benötigt wird) das Default-Objekt.

    Persönlich verwende ich diese Ansätze (Erzeugung mittels Code im Objekt selbst) ungern – insbesondere wenn es sich um projekt-spezifische Konfigurationen handelt. Du kannst dann zwar die Klasse gut testen – aber die Wiederverwendbarkeit leidet darunter. Denn in anderen Projekten hast du dann das falsche Default-Verhalten. Dieses Problem besteht mit Helper-Klassen nicht, weil jedes Projekt lediglich die Helper projekt-spezifisch anpassen muss, die Kern-Komponente aber unverändert ist.

    Schöne Grüße aus Freiburg

    Thomas

    Reply
  3. Guten Morgen,

    ich schließe mich da Thomas an. Benutze selber am liebsten auch eine „Factory?“ die mir das entsprechende Objekt dann zusammenbaut. Für Unit Tests kann man dann das Objekt manuell erstellen.

    Arne

    Reply
  4. @micha149

    Dein vorgehen ist auch nur Dependency Injection. Es gibt ja mehrere Möglichkeiten die Abhängigkeit zu injizieren.
    – Constructor Injection
    – Setter Injection
    – Property Injection

    @Nils

    Meiner Meinung nach lässt sich Dependency Injection nur ordentlich mit einem DIContainer umsetzen. Man definiert also in einer Config alle Abhängigkeiten. Diese werden dann durch den Container, automatisch in die zu erstellenden Objekte injiziert. Stubbles oder der Symfony DIContainer sind da gute Beispiele für.

    Reply
  5. Guten Morgen,

    ich nutze ebenfalls eine Factory um meine Objekte samt Abhängigkeiten zu erzeugen.
    Weiterhin hätte man dann die Möglichkeit relativ einfach auf einen DI-Container zu migrieren, da innerhalb der Klassen keine Instanzierungen vorgenommen werden.
    Für Unit-Tests gibt es dann eine Mock-Version der Factory falls nötig.

    Grüße
    Matthias

    Reply
  6. Kann mich dem nur anschließen: Ohne Container ist in Dependency-Injection in einer nicht-trivialen Applikation nicht umzusetzen.

    Allerdings finde ich den Ansatz von Spring (und ergo auch den von Symfony) zu umständlich: dort muss man sämtliche Abhängigkeiten explizit per Konfiguration (XML oder YAML) definieren. In der Praxis ist das, meiner Meinung nach, zu umständlich und unterbricht den Programmier-Fluss.

    Ich bin auch der Ansicht, dass man wenn dann alle Objekte in einem DI Container verwalten sollte und nicht nur einige von denen man im Moment glaubt dass sie DI benötigen.

    Mir gefällt – wie sollte es anders sein – der Ansatz den ich in FLOW3 verwende im Moment am besten. Dort gibt es ein Autowiring, das den Großteil der Abhängigkeiten selbst erkennt und konfiguriert (per Reflection). Diese Konfiguration wird dann anschließend als hard-kodierte PHP Datei gecached und stellt damit auch keinen Geschwindigkeitsnachteil dar.

    Hier gibt’s einige Beispiele: http://flow3.typo3.org/documentation/manuals/flow3/flow3.objectframework/#id38060194

    Reply
  7. @ Robert

    Wie machst du das beim Autowiring, wenn du mehrere konkrete Implementierungen einer Schnittelle verwendest.

    public function setRequest(RequestInterface $request) {}

    class HTTPRequest implements RequestInterface {}
    class RESTRequest implements RequestInterface {}
    class CLIRequest implements RequestInterface {}

    Dann kann die Abhängigkeit ja nicht mehr erkannt werden!? Da dein Container ja nicht weiß, welche konkrete Implementierung er injizieren soll.

    Reply
  8. @Robert: Spring erlaubt, wenn ich recht weiß, mittlerweile auch ein Wiring über Annotations. Allerdings bin ich kein Freund davon, auch wenn das Schreiben von XML Code etwas mehr Overhead ist mag ich die strikte Trennung zwischen Konfiguration und Programmcode.

    Reply
  9. @Nils Deine Logger-Klasse mit der Standardkonfiguration macht so wenig Sinn. Du verknüpfst deinen Code auf diese Weise schon wieder mit einer Depencency (auf die Logger-Klasse), schließlich musst du irgendwo Logger::getStandardLogger() rufen. D.h. spätestens im anderen Kontexten wie bsp. den Unittests hast du wieder eine fest codierte Abhängigkeit die dir wahrscheinlich Probleme machen wird. Ohne echten DI Container ist das kein richtiger Gewinn, da schließe ich mich den anderen Kommentaren an.

    Reply
  10. @Christian

    Wenn mehrere konkrete Implementierungen in Betracht kommen, gibt es kein Autowiring. In dem Fall muss man tatsächlich explizit per (YAML-) Konfiguration eine Klasse angeben:

    F3\SomePackage\RequestInterface:
    className: F3\MyPackage\RESTRequest

    Reply
  11. 1. Das „Problem“ hat man in der Praxis nicht:

    namespace Me {
    class CurrentLifeTimeFactory {
    protected $logger;

    public function giveMeObjectWithInjectedLogger() {
    return new \Me\Obj1($this->logger());
    }

    public function giveMeSomeOtherObjectWithInjectedLogger() {
    return new \Me\Obj2($this->logger());
    }

    // singleton!!! wuuuah magic 🙂
    public function logger() {
    if (!isset($this->logger)) {
    $this->logger = new \Me\Logger(new FileWriter(new LinuxWriter(new Ext3())));
    }
    return $this->logger;
    }
    }
    }

    2. „Setter-/Property-Injection“ _muss_ man gleich wieder vergessen:
    http://misko.hevery.com/2009/02/19/constructor-injection-vs-setter-injection/

    3. und zum Thema „auto“ Container etc.:
    http://blog.objectmentor.com/articles/2010/01/17/dependency-injection-inversion

    conclusion:
    eine Factory pro „Lebenszeit“ und gut ist: einfach und flexibel 🙂

    Reply
  12. Nils, schau dir auch mal yadif von benjamin eberlei an. Ist auch ein guter dependency injection container.
    Gibt es auf github.

    @micha149: nicht schon wieder! :-p

    Reply
  13. Ist DI nicht quasi per Definition über einen Container/Framework geregelt (habe ich zumindest noch so gelernt)? Was du hier beschreibst ist ja einfach ein IOC Ansatz und genau was du hier beschreibst ist der Vorteil eines DI Frameworks (und meiner Meinung nach nicht zuletzt Nachteil einer reinen Konstruktor Injection).

    Reply
  14. @Rüfenacht
    Nein, Dependency Injection sagt nichts mehr aus, als das, was der Name wieder gibt: Abhängigkeiten werden injeziert. Konstruktor, Setter, oder selbige mit Umweg über Container sind nur entsprechende Konzepte, die das Problem konkretisieren.
    Die oftmals in Java vertretene Variante, dass man konkrete Implementierungen über globale Properties angibt, gilt streng genommen auch schon dazu.

    Reply
  15. @KingCrunch: Das was Michael da geschrieben hatte, kam mir auch unbekannt vor. Dann habe ich auf Wikipedia geschaut und das gefunden:

    „Dependency Injection überträgt die Verantwortung für das Erzeugen und die Verknüpfung von Objekten an ein extern konfigurierbares Framework, entsprechend einem Komponentenmodell.“

    Auch wenn Wiki als wissenschaftliche Quelle ungeeignet ist, scheint er zumindest nicht der einzige mit der Meinung zu sein.

    Reply
  16. @Stephan Konfiguration per Annotations ist auch nicht unbedingt worauf ich hinaus wollte. Autowiring ohne jegliche Konfiguration ist das was wirklich angenehm ist:

    class Foo {

    public function __construct(\F3\MyPackage\Bar $bar) {

    }
    }

    Wenn Foo instantiiert wird ohne dass $bar übergeben wird, injected FLOW3 $bar automatisch.

    Reply
  17. @Robert Da haste natürlich recht. Wobei die Frage ist in wie vielen Fällen das am Ende wirklich funktioniert. Zum Einen wenn mehrere Implementierungen ein Interface definieren, zum Anderen wenn du skalare Werte in Objekte pushen willst (Applikationspfade, DB-Zugangsdaten usw.).

    Aus der täglichen Praxis (ich schule u.a. unsere Entwickler in dem Bereich) muss ich sagen dass für neue Entwickler das DI Pattern schon gewöhnungsbedürftig ist, wenn ich dann noch mit Autowiring ankomme verwirrt das wahrscheinlich noch viel mehr – Zu viel magic für den Anfang *g*

    Reply
  18. @Robert

    Für mich klingt das Autowiring auf den ersten Blick schön einfach. Wenn man aber länger überlegt halte ich das für mehr Aufwand gerade im Hinblick auf mehrere konkrete Implementierungen einer Schnittelle. Denn das bleibt wenn man ein Framework nutzt nicht aus. Das Problem sehe ich darin das man einen Mix aus Autowiring und der klassischen Injection verwenden muss. Hier habe ich auf jeden Fall größeren Dokumentationsaufwand. Denn ich muss mir irgendwo notieren wie welches Objekt injiziert wird. Wenn ich stattdessen alles zentral über eine Konfigurationsdatei konfiguriere spare ich mir den Aufwand.

    Reply
  19. Also eine statische create-Methode, die doch wieder selber die Abhängigkeiten setzt ist eine komplette Aushebelung des Prinzips. Damit hat man DI gebaut um sich eine Helpermethode zu schreiben, die sie wieder umgeht… 😉

    Besser ist hier wirklich ein Factory-Ansatz, der das ganze intern über eine Konfigdatei löst.

    Wobei man am Ende wirklich überlegen muss ob DI sooo sinnvoll für das eigene Projekt ist. Es ist ne tolle Sache, hebt aber alles auf eine deutlich höhere Abstraktionsstufe und bringt damit eine Menge Komplexität.

    In der Regel tauscht man eh nie wirklich was aus… Daher favorisiere ich persönlich einen konfigurativen Ansatz wo aber der Logger intern weiß wie er sich nun selber einrichten muss. Also dass ich ihn als Black-Box nutzen kann. Für mich gehört diese Logik eben doch ins Objekt hinein.

    Reply
  20. @Sven

    > Es ist ne tolle Sache, hebt aber alles auf eine deutlich höhere
    > Abstraktionsstufe und bringt damit eine Menge Komplexität.
    wenn etwas komplex ist, ist es falsch!

    > In der Regel tauscht man eh nie wirklich was aus…
    wie? du schreibst keine (Unit)Tests?

    > … der Logger intern weiß wie er sich nun selber einrichten muss.
    > … Für mich gehört diese Logik eben doch ins Objekt hinein.
    ocha… also „in eine Datei“ würd ich verstehen… „in eine Klassendefinition“ geht auch noch… aber „ins Objekt“?
    wie (er)zeugt man sich selber? (?.¿)

    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