Entwurfsmuster: Dependency Injection


Verbreitet die Message!

In nahezu jedem (Objekt-Orientierten) Projekt möchte man Klassen implementieren, die eine Abhängigkeit zu einer anderen Klasse aufweisen. Nehmen wir zur Übersichtlichkeit eine Klasse X her, die von der Klasse A abhängt, dann folgt daraus dass die Klasse X eine Instanz der Klasse A benötigt. Ein naiver Ansatz diese Abhängigkeit darzustellen, wäre es in der Implementierung der Klasse X anzugeben, wie Klasse A instanziiert wird.  Daraus entstehen jedoch einige Probleme.

Die abhängige Klasse X muss dann Implementierungsdetails der Klasse A kennen von der sie abhängt, beispielsweise wie diese zu instanziieren ist. Einerseits ist es bei großen System schlichtweg unübersichtlich und verwirrend die Abhängigkeiten korrekt händisch aufzulösen. Andererseits sind Abhängigkeiten oft Klassen auf deren Implementierung man keinen Einfluss hat. Verändern sich diese, muss man auch die Abhängigen verändern – das ist eine Katastrophe.

Außerdem möchte man bei (Unit-) Tests einer Klasse gerne die Abhängigkeiten “mocken” können, also nicht die realen Abhängigkeiten verwenden. Man könnte sonst nicht ausschließen ob Fehler von den Abhängigkeiten oder von dem herrühren was man eigentlich testen will.

Beispiel

Als Beispiel nehmen wir einen Service her, der Autodaten von einem Backend mit HTTP-Schnittstelle holen soll. Es bietet sich an, die bereits existierende Service-Klasse HttpService dafür zu verwenden – sie kommt aus einer Bibliothek von klugen Menschen, die die gesamte HTTP Kommunikation und Fehlerbehandlung für uns schon abgewickelt haben.

Bei unserem Beispiel könnte ein naiver Aufbau folgendermaßen ausssehen:

class AutodatenService(){
  private HttpService http;
  // Konstruktor
  public AutodatenService(){
  }

  private void init(){
    HttpConfig httpConfig = new httpConfig('URL.Zum.Autodaten.Backend.de/api/Auto', 'GET', 'application/json'); 
    HttpService http = new HttpService(httpConfig);
  }

  public AutoData getAutodaten(){
    return (AutoData) http.get();
  }

  // ..
}

Wir sehen hier, dass in der Funktion getAutodaten der HttpService mit einem Konfigurationsobjekt instanziiert wird. Daran sind zwei Dinge problematisch:

Einerseits ist es schlecht, dass wir wissen müssen, wie wir den HttpService konfigurieren müssen – eigentlich ist das nicht die Aufgabe des AutodatenService. Er soll die Autodaten holen und validieren – nicht einen Teil der HTTP-Kommunikation abwickeln. Andererseits ist unklar was passiert, wenn sich die Klasse HttpService ändert. Es kommt nicht selten vor, dass sich Klassen einer Bibliothek ändern, sodass der eigene Code nicht mehr mit der aktuellen Version zusammenpasst. Nun müssten wir also den AutodatenService ändern, falls sich Code ändert auf den wir keinen Einfluss haben.

Ein weiterer Punkt ist an folgendem Beispiel, das einen Service zeigt, der ein anderes Domain-Objekt verarbeitet:

class HerstellerDatenServce(){
  private HttpService http;
  // Konstruktor
  public HerstellerDatenServce(){
    init();
  }

  private void init(){
    HttpConfig httpConfig = new httpConfig('URL.Zum.Hersteller.Backend.de', 'GET', 'application/xml'); 
    this.http = new HttpService(httpConfig);
  }

  public HerstellerData getHerstellerDaten(){
    return validiereHerstellerDaten((HerstellerData) this.http.get());
  }

  private void validiereHerstellerDaten(HerstellerData herstellerData){
    // ..
  }
  
}

Wir haben leider Codedopplungen – und das wollen wir nicht, denn händisch alles aktuell zu halten wird spätestens nach der zehnten Anpassung nicht mehr funktionieren.

Was ist Dependency Injection?

Bei der Dependency Injection (geht zurück auf Martin Fowler) geht es nun darum, die Abhängigkeit der Datenservices mittels “Inversion of Control” abzubilden. Hinter diesem Begriff verbirgt sich lediglich, die Kontrollabgabe der abhängigen Klasse an eine andere Klasse, beispielsweise innerhalb eines Frameworks. Wir nennen diese im Beispiel “InjectorKlasse”.

Diese Klasse übernimmt für uns die Instanziierung der Abhängigkeiten (vgl. HttpService) und stellt diese den Abhängigen (vgl. AutoDatenService, HerstellerDatenService) zur Verfügung.

Dies geht prinzipiell auf drei unterschiedliche Weisen.

Constructor Injection

Bei der Constructor Injection wird der Abhängigen die Abhängigkeit über den Konstruktor zur Verfügung gestellt:

class AutodatenService(){
  private HttpService http;

  // Konstruktor
  public AutodatenService(HttpService http){
    this.http = http;
  }

  public AutoData getAutodaten(){
    return (AutoData) http.get();
  }
}

Wir sehen hier, dass die Klasse AutodatenService nichts über den HTTP-Service wissen muss. Natürlich muss man sich jetzt fragen, woher dann die Config-Informationen für den AutoDatenService kommen können.
Dies geschieht in der Injector-Klasse, die innerhalb der Anwendung verwendet wird. Beispielhaft könnte dies so aussehen:

class Injector{
  void init() {
    HttpConfig httpConfig = new httpConfig('URL.Zum.Autodaten.Backend.de', 'GET', 'application/json'); 
    HttpService http = new HttpService(httpConfig);
    
    AutoDatenService autoDatenService= new AutoDatenService (http);
    /* Weitere Abhängigkeiten */
  }
}

Der Injector bildet die korrekte Abhängigkeitshierarchie ab. Ist dies nicht möglich, ist zur Kompilierzeit klar, dass die Anwendung so nicht funktionieren kann (beispielsweise weil Klasse A von B abhängt, Klasse B aber von Klasse A). Das ist gut, da Fehler hier zentral auffallen – entkoppelt von der Business Logik.

Im realen Beispiel sieht man zusätzlich, dass der Injector neue Möglichkeiten hat, beispielsweise kann er unterschiedlichen DatenServices (oder anderen Abhängigkeiten) die gleichen Instanz des HttpService zur Verfügung stellen (siehe Singleton) oder eben jeder Abhängigkeit eine neue Instanz garantieren.

Setter Injection

Ausgehend von der Constructor Injection ist die Setter Injection nur eine andere Form, das Gleiche anzuwenden.

class AutodatenService(){
  private HttpService http;

  // Konstruktor
  public AutodatenService(){
  }

  public void setHttp(HttpService http){
    this.http = http;
  }

  public AutoData getAutodaten(){
    return (AutoData) http.get();
  }
}

Es wird nicht über den Konstruktor, sondern über einen Setter injiziert.

class Injector{
  void init() {
    HttpConfig httpConfig = new httpConfig('URL.Zum.Autodaten.Backend.de', 'GET', 'application/json'); 
    HttpService http = new HttpService(httpConfig);
    
    AutoDatenService autoDatenService= new AutoDatenService ();
    autoDatenService.setHttp(http);
   /* Weitere Abhängigkeiten */
  }
}

Interface Injection

Kaum anders sieht die Interface Injection aus:

class AutodatenService() implements IHttpServiceInjectable{
  private HttpService http;

  // Konstruktor
  public AutodatenService(){
  }

  public void injectHttpService(HttpService http){
    this.http = http;
  }

  public AutoData getAutodaten(){
    return (AutoData) http.get();
  }
}

Hier wird ein Interface benutzt, dass die Implementierung einer Methode vorschreibt, die dem Setter ähnlich ist:

interfacte IHttpServiceInjectable{
  void injectHttpService(HttpService http);
}

Nur der Name unterscheidet hier die Methodensignatur vom Setter.

Auch der Injector sieht wieder kaum anders aus:

class Injector{
  void init() {
    HttpConfig httpConfig = new httpConfig('URL.Zum.Autodaten.Backend.de', 'GET', 'application/json'); 
    HttpService http = new HttpService(httpConfig);
    
    AutoDatenService autoDatenService= new AutoDatenService ();
    autoDatenService.injectHttpService(http);
   /* Weitere Abhängigkeiten */
  }
}

Anmerkung zum Injector

Der Injector ist hier nur beispielhaft skizziert und würde so in der realen Entwicklung wahrscheinlich nicht vorkommen. Stattdessen fragt der Injector nach einer Instanz einer Abhängigkeit im Kontext der beiden Klassen an und bekommt diese dann.

Fazit

Dependency Injection ist ein sehr weit verbreitetes Entwurfsmuster, da es Abhängigkeiten sauber und performant auftrennen kann. Es erhöht die Wartbarkeit, Skalierbarkeit und Ermöglicht einfaches Testen. Damit gehört es in den Werkzeugkasten jedes Developers. Die bekannten Frameworks Spring und Angular nutzen diese Muster sehr häufig. Ich empfehle, Beispielprojekte damit zu implementieren umd mit dem Muster warm zu werden.

Verbreitet die Message!

Schreiben Sie einen Kommentar