Ankündigung

Einklappen
Keine Ankündigung bisher.

Design Entscheidung: Dependency Injection Container

Einklappen

Neue Werbung 2019

Einklappen
X
  • Filter
  • Zeit
  • Anzeigen
Alles löschen
neue Beiträge

  • Design Entscheidung: Dependency Injection Container

    Guten Abend,

    ich habe als Freizeitprojekt einen eigenen Dependency Injection Container entwickelt. Dieser bietet verschiedene Funktionen (Constructor- und Property-Injection mit und ohne Annotationen etc.) an. Dieser Container ermittelt über die Reflection API die Struktur einer Klasse und ermittelt die erforderlichen Abhängigkeiten. Diese werden dann rekursiv injiziert. Das Spiel kennt ihr denke ich sicher. Aktuell stehe ich jedoch vor einer design-technischen Frage, über die ich mir persönlich noch nicht im Klaren bin. Aus diesem Grund möchte ich euch fragen, welche der beiden Varianten, die ich gleich vorstellen werde, ihr bevorzugen würdet - sofern möglich aus dem Bauch heraus, da ihr die Implementierung ja nicht kennt.

    Ich möchte diesen automatisierten Prozess von Dependency Injection ein wenig weiter fassen und diesen Prozess flexibel gestalten. Salopp gesagt sollte die Möglichkeit bestehen, diesen Prozess um neue Funktionen erweitern zu können. Das bedeutet: In eine Klasse sollen Abhängigkeiten injiziert werden, die eventuell andere Informationen, andere Zustände und einen anderen Kontext als der "Standard"-Container benötigen. In diesem Sinne bieten sich meiner Meinung nach zwei unterschiedliche Lösungsansätze an.

    a) Es kann mehrere unabhängige Dependency Injection Container Implementierungen geben, die in einer bestimmten Reihenfolge (Kette) ausgeführt werden und gemeinsame Informationen teilen.

    Struktur:

    ContainerChain {ContainerA, ContainerB, ... }

    b) Es gibt nur einen Dependency Injection Container, welcher plugin-fähig sein könnte. Hierbei könnten sich Plugins in den Container und die Subkomponenten registrieren und Aufgaben in ihrer eigenen Dömane verrichten.

    Struktur:

    ContainerA { Plugins: { ConstructorProcessor, PropertyProcessor, ... } }


    Für beide Ansätze sprechen nach meinem Dafürhalten verschiedene Vor- und Nachteile. Der Vorteil von der Variante a) ist sicherlich, dass beide Container komplett koexistent sind und sich lediglich einen gemeinsamen Pool an Informationen teilen. Der Nachteil ist jedoch, dass dafür ein zusätzlicher Implementierungsaufwand anfallen wird (was jedoch nicht bedeutet, dass ich diesen nicht in Kauf nehmen würde). Der Vorteil von der Variante b) hingegen liegt darin, dass eine bereits bestehende Implementierung genutzt und erweitert werden kann; demzufolge muss nicht der komplette Stack eventuell neu programmiert werden. Nachteil ist jedoch, dass wahrscheinlich niemals die Flexibilität wie mit der Variante a) geschaffen werden kann.

    Spricht denn prinzipiell etwas dagegen, den _einen_ Container plugin-fähig zu gestalten? Ich hoffe, dass meine Frage ausreichend erklärt ist. Ansonsten muss ich ein wenig weiter ausholen. Für Anregungen und Ideen bin ich sehr dankbar.

    Gruß, Anyone

  • #2
    Hallöchen,

    kannst du vielleicht zu den beiden genannten Varianten, oder generell zu dem Problem das du lösen willst, simple Code-Beispiele erstellen? Ich denke das würde deine Idee besser verdeutlichen. Ich bin mir nämlich nicht sicher, ob ich dein Vorhaben korrekt interpretiere.

    Viele Grüße,
    lotti
    [SIZE="1"]Atwood's Law: any application that can be written in JavaScript, will eventually be written in JavaScript.[/SIZE]

    Kommentar


    • #3
      Guten Tag,

      gerne kann ich auch mein Vorhaben ein wenig näher beleuchten. Dabei geht es in gewisser Hinsicht auch um den Begriff "Dependency Injection" selbst. Die Frage, die sich mir persönlich unter anderem stellt ist, wann "klassisches" Dependency Injection endet und welche Aspekte noch zu der klassischen Idee dahinter gehören. Wahrscheinlich verbinden viele Entwickler mit dem Begriff "Dependency Injection Container" eine Sammlung von Klassen und Methoden, die ein Hauptziel verfolgen: Die automatisierte Injizierung von Objekten in andere Objekte, sodass dieses Vorhaben meist nicht mehr manuell realisiert werden muss. Es ist also ohne weiteres möglich einen Baum von Objekten erstellen zu lassen. Allerdings gibt es verschiedene Frameworks und Dependency Injection Container, die darüber hinaus noch einen Schritt weiter gehen. Diese ermöglichen zusätzlich u.a. die Injizierung von Konfigurationsparametern.

      Diese beiden Konzepte (Injizierung von Objekten & Injizierung von Konfigurationsparametern) sind meiner Meinung nach unterschiedlich. Beide Konzepte verfolgen im eigentlichen Sinne dasselbe Ziel, nämlich die Injizierung von Abhängigkeiten; allerdings unterscheidet sich die dahinterstehende Implementierung. Eine Gemeinsamkeit ist, dass sich beide Implementierung wahrscheinlich dieselben oder zumindest ähnliche Informationen teilen. Das wäre dann auch schon der genannte erste Vorschlag aus dem Eröffnungsbeitrag. Dies lässt sich wahrscheinlich am folgenden Quellcode näher verdeutlichen.

      Variante 1)

      PHP-Code:

      class ContainerDTO {
          private 
      $obj null;

          public function 
      setObj($obj) {
               
      $this->obj $obj;
          }  

          public function 
      getObj() {
               return 
      $this->obj;
          }

          
      // weitere Methoden und attribute
      }

      class 
      ContainerChain {

          private 
      $containerList = [];


          public function 
      addContainer(ContainerInterface $container) {
               
      $this->containerList[] = $container;
          }

          public function 
      process($class) {
               
      $dto = new ContainerDTO();

              foreach (
      $this->containerList as $container) {
                    
      $container->setDTO($dto);
                    
      $container->get($class);
              }
          }

      }

      class 
      DIContainer implements ContainerInterface {

          private 
      $dto null;


          public function 
      setDto(ContainerDTO $dto) {
                
      $this->dto $dto;
          }


          public function 
      get($class) {
                
               
      $obj = ...

               
      $this->dto->setObj($obj);
               
               return 
      $obj;
          }

          
      // Implementierungsdetails

      }

      class 
      ConfigurationDIContainer implements ContainerInterface {

          
      // Implementierungsdetails, Aufbau ähnlich dem DIContainer was das DTO betrifft

      }

      class 
      Bar {

      }

      class 
      Foo {

          
      /**
           * @Config(Session.Lifetime)
           */
          
      private $config;

          public function 
      __construct(Bar $bar) {

          }

      }


      $chain = new ContainerChain();
      $chain->addContainer(new DIContainer());
      $chain->addContainer(new ConfigurationDIContainer());
      $chain->process(Foo::class); 
      Der zuerst registrierte Container ist für die Erstellung eines Objektes der Klasse Foo verantwortlich und injiziert dessen Objekt-Abhängigkeiten. Dieser Container setzt das erstellte Foo-Objekt in das DTO auf das der zweite Container (ConfigurationDIContainer) zugreifen kann. Diese Klasse kann dann das Objekt aus dem DTO ziehen und die nötigen Konfigurationen in das Objekt injizieren.


      Variante 2)

      Eine zweite Möglichkeit könnte darin bestehen, zu definieren, dass es bei _einem_ einzigen Container bleibt. Dieser Container bietet Erweiterungsmöglichkeiten über Plugins.

      PHP-Code:

      class Container {
             
          private 
      $plugins = [];


          public function 
      registerPlugin(ContainerPlugin $plugin) {
                
      $this->plugins[] = $plugin;
          }

          public function 
      get($class) {
                
      $obj $class;

                foreach (
      $this->plugins as $plugin) {
                       
      $obj $plugin->get($obj);
                }
          }

      }

      class 
      ClassicDIPlugin() {
           
      // für die "Klassische" DI-Variante
      }

      class 
      ConfigDIPlugin {
           
      // für die Injizierung von Konfigurationsparametern
      }

      class 
      Bar {

      }

      class 
      Foo {

          
      /**
           * @Config(Session.Lifetime)
           */
          
      private $config;

          public function 
      __construct(Bar $bar) {

          }

      }

      $container = new Container();
      $container->registerPlugin(new ClassicDIPlugin());
      $container->registerPlugin(new ConfigDIPlugin());
      $container->get(Foo::class); 

      Ich hoffe, dass dieser Quellcode nun sprechend genug für die beiden verschiedenen Varianten ist. Mir ist nämlich nicht klar, für welche der beiden Varianten ich mich entscheiden soll. Beide bieten Vor- und Nachteile.

      Kommentar


      • #4
        Ich muss sagen, dass ich auch nach dem zweiten Post das eigentliche Problem noch nicht verstanden habe. Sorry, falls mein Post dann ein wenig an dem Sinn des Threads vorbeiführt. Ich sehe hier folgende Punkte:
        • Es gibt praktisch nur einen Weg, Abhängigkeiten zu injizieren: Constructorinjection. Verabschiede dich Gedanklich von Plugins, Annotations, Propertyinjection und Methodinjection. Bei Methodinjection gibt es zumindest in ein paar sehr wenigen Fällen tatsächlich Bedarf, aber das kann man in der Regel dann auch über einen anderen Weg lösen.
        • Du hast deinen eigenen DIC geschrieben. Wenn du ihn mit PHP-DI vergleichst, was ist an ihm dann besonders? Für mich ist PHP-DI einer der mächtigsten und einfachsten Container derzeit. Es wäre für mich einfacher, wenn du mir sagst, wodurch sich dein Container unterscheidet. Besonders in Hinsicht auf Autowiring und LazyProxy-Instantiation.


        PHP-Code:
        $container->get(Foo::class); 
        Ist dir der Unterschied zwischen einem ServiceLocator und einem DependencyInjectionContainer bewusst?

        Kommentar


        • #5
          Ich muss sagen, dass ich auch nach dem zweiten Post das eigentliche Problem noch nicht verstanden habe.
          Kein Problem, ich habe mich mittlerweile mit der zweiten Variante angefreundet.

          • Es gibt praktisch nur einen Weg, Abhängigkeiten zu injizieren: Constructorinjection.
          • Halte ich persönlich für gewagt. Sicherlich führt ein Konstruktor die notwendigen Abhängigkeiten für eine Klasse auf. Was ist jedoch, wenn eine Klasse über mehr als beispielsweise 'nur' fünf Abhängigkeiten besitzt? Klar kann mit dem Single-Responsibility-Prinzip und Separation of Concerns argumentiert werden. In der Praxis hat sich dies, zumindest bei meiner Arbeitsstelle, für schwierig herausgestellt. Natürlich ist dort bei weitem nicht alles in Gold gegossen, jedoch entstehen teilweise komplexe Zusammenhänge in den Use-Cases, die eine Vielzahl von verschiedenen Service-Klassen benötigen. Soll der Konstruktor dann über 20 Parameter verfügen? Ich denke nicht, dass ein Programmierer über einen Konstruktor mit über 20 Parametern erfreut sein wird. In der Theorie hast du natürlich Recht.

            Verabschiede dich Gedanklich von Plugins,
            Wieso? Plugins sind dafür da, bestehende Funktionen zu erweitern. Solange sie dieses Ziel in einem dafür vorgesehenen Rahmen verrichten ist doch alles prima.

            Annotations,
            Das ist sicherlich ein Punkt, über den sich schwer streiken lässt. Der von dir zitierte PHP-DI-Container bietet nebenbei angemerkt auch Annotationen an.

          • Du hast deinen eigenen DIC geschrieben. Wenn du ihn mit PHP-DI vergleichst, was ist an ihm dann besonders? Für mich ist PHP-DI einer der mächtigsten und einfachsten Container derzeit. Es wäre für mich einfacher, wenn du mir sagst, wodurch sich dein Container unterscheidet. Besonders in Hinsicht auf Autowiring und LazyProxy-Instantiation.
        Muss denn mein Projekt einen besonderen Unterschied zu bestehenden Implementierungen aufweisen? Wie bereits erwähnt ist dies ein Freizeitprojekt, welches sicherlich auch den ein oder anderen Lernerfolg erzielen möchte und auch bereits hat.

        PHP-Code:
        $container->get(Foo::class); 
        Ist dir der Unterschied zwischen einem ServiceLocator und einem DependencyInjectionContainer bewusst?
        Klar. Bei einem ServiceLocator wird dieser Locator direkt benutzt, Dependency-Injection-Container füllt die Abhängigkeiten indirekt - gemäß dem Inversion of Control Prinzip. Der Aufruf der 'get' Methode ist als initialer Aufruf zu verstehen.


        Ich habe mich nun entschieden, eher auf Plugins statt einem zweiten oder dritten Container zu setzen. Ich stelle entsprechende Hookpoints in den Implementierungsklassen bereit, die von den einzelnen Plugins bedient werden können. Somit entsteht eine nach meinem Dafürhalten saubere Trennung von Implementierungen, die keinen weiteren Container erfordert.

        Kommentar


        • #6
          Zitat von Anyone
          Kein Problem, ich habe mich mittlerweile mit der zweiten Variante angefreundet.
          Mich würde ernsthaft der Hintergrund zur Notwendigkeit von Plug-ins interessieren. Vielleicht lerne ich noch einen weiteren UseCase kennen, vielleicht habe ich auch eine andere Perspektive für dich...


          Zitat von Anyone
          Halte ich persönlich für gewagt. Sicherlich führt ein Konstruktor die notwendigen Abhängigkeiten für eine Klasse auf. Was ist jedoch, wenn eine Klasse über mehr als beispielsweise 'nur' fünf Abhängigkeiten besitzt? Klar kann mit dem Single-Responsibility-Prinzip und Separation of Concerns argumentiert werden. In der Praxis hat sich dies, zumindest bei meiner Arbeitsstelle, für schwierig herausgestellt. Natürlich ist dort bei weitem nicht alles in Gold gegossen, jedoch entstehen teilweise komplexe Zusammenhänge in den Use-Cases, die eine Vielzahl von verschiedenen Service-Klassen benötigen. Soll der Konstruktor dann über 20 Parameter verfügen? Ich denke nicht, dass ein Programmierer über einen Konstruktor mit über 20 Parametern erfreut sein wird. In der Theorie hast du natürlich Recht.
          Dein Tipp ist richtig, ich würde das Klassendesign beanstanden. In schlimmsten Fall hätte der Construtor tatsächlich 30 Abhängigkeiten, dafür hätte das Objekt nach der Erstellung schon alle Informationen, die es braucht um danach Immutable zu sein. Langfristig wird man aber wesentlich mehr Vorteile daraus ziehen, sich eingangs intensiver mit dem Design seiner Klassen beschäftigt zu haben.

          Zitat von Anyone
          Das ist sicherlich ein Punkt, über den sich schwer streiken lässt. Der von dir zitierte PHP-DI-Container bietet nebenbei angemerkt auch Annotationen an.
          Ja, es gibt aber auch die Methode useAnnotations(false). mnapoli hat jetzt auch erbarmen gezeigt und mit Version 5 von PHPDI diese Funktion standardmäßig deaktiviert.

          Zitat von Anyone
          Muss denn mein Projekt einen besonderen Unterschied zu bestehenden Implementierungen aufweisen? Wie bereits erwähnt ist dies ein Freizeitprojekt, welches sicherlich auch den ein oder anderen Lernerfolg erzielen möchte und auch bereits hat.
          Alles gut! Ich hätte nur evtl schneller in das Thema gefunden, wenn diese Frage für mich klar wäre.

          Kommentar


          • #7
            Tatsächlich gibt es noch einige Use-Cases, was allerdings den Rahmen meiner Frage sprengen würde. Ich kann dir jedoch gerne per PN mehr erzählen, sofern Interesse besteht.

            Der von mir entworfene Container besitzt wahrscheinlich noch keine Wow-Merkmale, ich bin jedoch zuversichtlich, dass mit der Zeit interessante Features implementiert werden können. Jedenfalls gibt es ein Plugin für den Container, welches rekursive zirkuläre Referenzen entdecken und mit einer entsprechenden Exception reagieren kann. Ich weiß nicht, ob und inwiefern PHP-DI dafür etwas hat. Ist auch mehr Spielerei als alles andere.

            Auch kann der Container mit Autowiring und Constructor-Injektion umgehen. Parameterinjection oder Annotationen sind keinesfalls ein Muss, eher ein kann.

            Die Frage ist halt, bis zu welchem Grad Constructorinjection funktionieren kann. Wenn eine Klasse einmal im Konstruktor eine Abhängigkeit benötigt, die keinen eindeutigen Typehint hat, dann wird es ohne Annotationen oder externe Definitionen schwierig.

            Gruß, Anyone

            Kommentar

            • Lädt...
              X