Dependency Injection mit Dagger

Dagger ist ein quelloffenes Dependency-Injection-(DI-)Framework, das auf Guice basiert. Allerdings waren die Entwickler von Dagger nicht mit dem Grundprinzip von Guice zufrieden: Immer wieder hatten sie in größeren Projekten Unmengen an Binding-Code zu schreiben. Da dies aber Teil der statischen Semantik ist, verfolgen sie die Strategie, dieses Binding zu extrahieren und in Form von Quelltext abzulegen. Ein weiteres Entwurfsziel ist die gute Lesbarkeit und Unterstützung beim Debugging-Prozess. Im Folgenden werden wir uns Dagger genauer ansehen und die ersten Schritte mit dem Framework gehen. Wir beschäftigen uns hier mit Dagger ab der Version 1.2.
Eine der Besonderheiten von Dagger ist dessen Verfügbarkeit in Java- und Android-Systemen. Sehen wir uns ein einfaches Beispiel an und beginnen mit einem simplen @Inject einer Instanz (MainService) in unsere Main-Klasse:
public class Main { @Inject MainService mainService; /.. } public interface MainService { public String execute(String txt); } public class MainServiceImpl implements MainService { public String execute(String txt){ return "MainServiceImpl _ " + txt; } }
Der erste Schritt, um eine Instanz einer Klasse mit injizierten Attributen zu erhalten, ist die Verwendung des Objekt-Graphen. Der ObjectGraph ist die Repräsentation aller Abhängigkeiten, die für eine Instanziierung notwendig sind. Im folgenden Listing ist die Initialisierung und Anwendung von Dagger in der Klasse Main zu sehen.
public class Main { @Inject MainService mainService; public static void main(String[] args) { //bootstrapping ObjectGraph objectGraph = ObjectGraph.create(new BusinessModule()); Main main = objectGraph.get(Main.class); // Main main = new Main(); // objectGraph.inject(main); System.out.println("execute = " + main.mainService.execute("Und Los")); System.out.println("execute = " + main.mainService.execute("Und Los")); System.out.println("execute = " + main.mainService.execute("Und Los")); } }
Es gibt bei Dagger zwei Wege, um an eine Instanz einer Klasse zu gelangen, die injizierte Attribute enthält. Der eine Weg geht über die Methode get(..) der Klasse ObjectGraph und liefert direkt eine fertige Instanz. Der andere Weg geht über den expliziten Aufruf der Methode inject(..) und kann auf bestehende Instanzen angewendet werden. Damit kann man den Zeitpunkt zwischen der Erzeugung der haltenden Klasseninstanz und den Membern selbst kontrollieren.
Es gibt noch eine Kleinigkeit zu beachten: Zur Erzeugung einer Instanz der Klasse ObjectGraph wird als Parameter der Methode create(..) noch ein Modul benötigt. In unserem Fall new BusinessModule(). In diesem Modul werden die Abhängigkeiten untereinander definiert; ebenso die Producer, die verwendet werden sollen.
@Module(library = false, injects = {Main.class}, complete = true) public class BusinessModule { @Provides MainService provideMainService() { return new MainServiceImpl(); } }
In dem Producer kann entweder die Instanz selbst erzeugt werden, oder man kann Injection über Methodenparameter verwenden (nachfolgendes Listing). Hier ist Folgendes zu beachten: Bei Dagger muss man entweder den new-Konstruktor verwenden – wenn die zu erzeugende Instanz selber keine Attribute hat, die injiziert werden – oder eine Instanz per Methodenparameter geben lassen, wenn diese Instanz selbst Attribute injiziert bekommt.
@Provides MainService provideMainService(MainServiceImpl mainService) { return mainService; }
Interne Funktionsweise von Dagger
Die grundlegende Frage, die sich einem Entwickler recht schnell stellt, ist diejenige der Definition der Abhängigkeiten. Es gibt in Java verschiedene Wege und Zeitpunkte, um die Abhängigkeiten zu definieren. Handelt es sich um eindeutige Abhängigkeiten, ist es meist einfach. Gibt es Fallunterscheidungen oder Multiplizitäten, wird es schon komplizierter.
Erweitern wir unser Beispiel um ein Interface SubService. Für dieses Interface gibt es zwei Implementierungen: SubServiceA und SubServiceB. Wenn man nun im Quelltext ein @Inject SubService verwendet, ist es nicht mehr eindeutig, welche Implementierung gemeint ist. Gehen wir im Folgenden davon aus, dass es zur Erzeugung einer Instanz immer eine korrespondierende Methode provideXXX (in unserem Beispiel provideMainService()) gibt, die mit der Annotation @Produces versehen worden ist. Es handelt sich hierbei um eine Factory-Methode.
public class MainServiceImpl implements MainService { @Inject SubService subService; public String execute(String txt){ return subService.work(txt); } }
Ebenfalls wird eine Factory-Methode für die Erzeugung der Instanz von SubService benötigt. Hier kann man sich für eine Implementierung (SubServiceA oder SubServiceB) entscheiden, die verwendet werden soll. Das ist natürlich eine statische Entscheidung. Zu einem späteren Zeitpunkt werden wir dies dynamisch gestalten.
@Provides SubService provideSubService() { return new SubServiceA(); }
Wie aber erkennt Dagger, welche Instanz nun wo verwendet werden soll? Reflection oder Konfiguration ist hier die Antwort. In manchen Frameworks wird der Ansatz der Konfiguration verwendet. Diese Konfiguration wird dann ausgelesen und als Basis für die Instanziierung verwendet. Solche Ansätze haben leider immer das Problem, dass sie umständlich und fehleranfällig sind.
Die Alternative: Reflection. Zur Laufzeit kann ermittelt werden, welche Klasse welche Abhängigkeiten hat. Die Auflösung der Multiplizitäten ist damit noch nicht gelöst. JSR-330 hat hierfür den Qualifier @Named(..) vorgesehen. Damit kann man den jeweiligen Implementierungen Namen geben, die dann bei der Auflösung verwendet werden. Geben wir der Implementierung SubServiceA ein @Named(„A“) und der Implementierung SubServiceB ein @Named(„B“) mit. Nun muss man an der Stelle, an der ein @Inject definiert wird, zusätzlich den Qualifier angeben, z. B.
@Inject @Named(„A“) SubService subservice;
Die dazugehörige Factory-Methode wird ebenfalls mit @Named(„A“) versehen. Und genau über die Kombination Klasse/Interface und Annotation findet dann das Matching statt.
Damit man nun wieder ein einfaches
@Inject SubService subservice
verwenden kann, muss man noch eine Factory-Methode ohne Qualifier schreiben.
@Module(library = false, injects = {Main.class},complete = true) public class BusinessModule { @Provides MainService provideMainService(MainServiceImpl mainService) { return mainService; } @Provides @Named("A") SubService provideSubServiceA() { return new SubServiceA(); } @Provides @Named("B") SubService provideSubServiceB() { return new SubServiceB(); } @Provides SubService provideSubService(@Named("A") SubService subServiceA) { return subServiceA; } }
Wie bzw. wann werden nun die Abhängigkeiten erkannt? Das kann zur Laufzeit mittels Reflection realisiert werden. Dagger geht einen etwas anderen Weg: Reflection wird zwar verwendet, aber nicht zur Laufzeit. Es wird in den compile-Prozess eingegriffen. Die Abhängigkeiten werden mithilfe von Reflection aufgelöst und auch überprüft. Dieser Prozess erzeugt verschiedene Ergebnisse. Zum einen wird eine Datei mit der Endung dot angelegt. Dort kann man die gefunden Abhängigkeiten nachlesen. In unserem Fall die folgenden:
digraph G1 { concentrate = true; Main -> MainService; MainService -> MainServiceImpl; MainServiceImpl -> SubService; n2 [label="@javax.inject.Named(value=A) /org.rapidpm.demo.dagger.business.subservice.SubService"]; SubService -> n2; }
Die Verbindungen sind leicht zu erkennen. Aber was bedeuten die Klassen, die generiert werden? Gemeint sind: Main$$InjectAdapter , MainServiceImpl$$InjectAdapter und BusinessModule$$ModuleAdapter.
Beginnen wir mit der Klasse Main$$InjectAdapter. Der Adapter bietet eine Methode get(), die eine fertig instanziierte Referenz zurückgibt. Hier kann man den Lifecycle genau sehen – und der ist recht übersichtlich. Er besteht genau aus zwei Schritten: Erstens wird die Instanz erzeugt, zweitens die Methode injectMembers(..) aufgerufen. Außerdem ist dort die Verbindung zu den anderen Klassen abgebildet. Damit ist alles in der statischen Semantik, und zur Laufzeit wird ein entsprechender Getter aufgerufen. Dieser Ansatz ist zur Laufzeit natürlich wesentlich performanter als die Verwendung von Reflection. Diese Adapter-Klassen bilden also das vollständige Binding inklusive aller Kombinationen bzw. Varianten der Applikationen ab.
Aufmacherbild: Ancient sword with the bronze handholdon a beautiful background von shutterstock.com / Urheberrecht: Sergey856
[ header = Praktischer Einsatz, Besonderheiten von Dagger ]
Praktischer Einsatz von Dagger
Nun sehen wir uns an, wie man sich zur Laufzeit für eine Implementierung entscheiden kann. Wir verwenden hier den Ansatz eines Context-Objekts. In diesem Fall ist es ein Singleton, in dem ein Attribut vom Typ boolean gehalten wird. In Abhängigkeit vom Wert wird in der Produces-Methode die eine oder andere Implementierung verwendet.
@Provides SubService provideSubService(@Named("A") SubService subServiceA, @Named("B") SubService subServiceB) { if(Context.getInstance().defaultImpl){ return subServiceA; } else{ return subServiceB; } } //Ausgabe SubServiceA = 2014-11-21T14:15:56.364 SubSubServiceImpl => 2014-11-21T13:15:56.396Z SubServiceB = 2014-11-21T14:15:56.445 SubSubServiceImpl => 2014-11-21T13:15:56.445Z execute = Und Los_A_SubSubServiceImpl Und Los execute = Und Los_A_SubSubServiceImpl Und Los execute = Und Los_A_SubSubServiceImpl Und Los
Der Nachteil hier ist die gleichzeitige Instanziierung der beiden Implementierungen, obwohl nur eine benötigt wird. Ebenfalls ist zu sehen, dass die Instanzen immer von außen nach innen instanziiert werden. In unserem Fall wird erst der SubService, dann der SubSubService erzeugt.
Lazy<T>, der virtuelle Proxy
Man kann hier auch auf die Implementierung des Virtual Proxy zurückgreifen. In dem Fall wird eine Instanz erst bei Zugriff auf den Proxy angelegt. Danach jedoch liefert der Proxy immer dieselbe Instanz zurück. Es wird also nur der erste Zugriff zeitlich so lange verzögert, bis Bedarf besteht.
@Provides SubService provideSubService(@Named("A") Lazy subServiceA, @Named("B") Lazy subServiceB) { if (Context.getInstance().defaultImpl) { return subServiceA.get(); } else { return subServiceB.get(); } } SubServiceA = 2014-11-21T14:26:44.567 SubSubServiceImpl => 2014-11-21T13:26:44.588Z execute = Und Los_A_SubSubServiceImpl Und Los execute = Und Los_A_SubSubServiceImpl Und Los execute = Und Los_A_SubSubServiceImpl Und Los
Provider<T>, die Fabrik
Manchmal möchte man die Fabrik bzw. den Provider selbst zur Verfügung haben, um beliebig viele Instanzen programmatisch erzeugen zu können. Hierfür existiert die Klasse Provider. Auch hier handelt es sich wieder um einen Proxy, der mittels Aufruf der Methode get() veranlasst wird, eine neue Instanz zu erzeugen.
public class Main { @Inject Provider mainService; public static void main(String[] args) { final ObjectGraph objectGraph = ObjectGraph.create(new BusinessModule()); final Main main = objectGraph.get(Main.class); System.out.println("execute = " + main.mainService.get().execute("Und Los")); System.out.println("execute = " + main.mainService.get().execute("Und Los")); System.out.println("execute = " + main.mainService.get().execute("Und Los")); } } SubServiceA = 2014-11-21T14:39:47.146 SubSubServiceImpl => 2014-11-21T13:39:47.161Z execute = Und Los_A_SubSubServiceImpl Und Los SubServiceA = 2014-11-21T14:39:47.178 SubSubServiceImpl => 2014-11-21T13:39:47.178Z execute = Und Los_A_SubSubServiceImpl Und Los SubServiceA = 2014-11-21T14:39:47.179 SubSubServiceImpl => 2014-11-21T13:39:47.179Z execute = Und Los_A_SubSubServiceImpl Und Los
Modularer Aufbau
Bisher haben wir in einer Modulklasse alle erzeugenden Methoden zusammengefasst. Allerdings benötigt man bei größeren Implementierungen sicherlich eine Menge dieser Methoden. Um diese Methoden auf verschiedene Module zu verteilen, kann man in dem jeweiligen Obermodul die benötigten Submodule mit einbinden. Das passiert in der Klassenannotation @Module.
@Module( injects = {Main.class}, includes = SubServiceModule.class ) public class BusinessModule { @Provides MainService provideMainService(MainServiceImpl mainService) { return mainService; } }
Dieser Qualifier hat noch mehr mögliche Attribute. Hier allerdings benötigen wir erst einmal nur das Attribut includes. Dabei kann man alle Submodule angeben, die benötigt werden. Das Attribut injects gibt an, für welche Klasse dieses Modul zuständig ist – in unserem Fall für die Klasse Main, da hier der initiale @Inject stattfindet. Der rekursive Abstieg selbst muss dann nicht mehr angegeben werden.
Nun sind wir in der Lage, einzelne Module zu erzeugen und beliebig zusammenzubauen. Zusätzlich lässt sich der Zeitpunkt beeinflussen, zu dem die jeweiligen Objekte erzeugt werden. Das Umschalten der Implementierung funktioniert über die aktive Auswahl aufgrund von Entscheidungskriterien in dem jeweiligen Provider. Wichtig zu wissen ist, dass eine Instanz der Modul-Klasse selbst keine @Inject-Anweisungen verarbeitet.
public class Main { @Inject Provider mainService; public static void main(String[] args) { final ObjectGraph objectGraph = ObjectGraph.create(new BusinessModule()); final Main main = objectGraph.get(Main.class); System.out.println("execute = " + main.mainService.get().execute("Und Los")); Context.getInstance().defaultImpl = false; System.out.println("execute = " + main.mainService.get().execute("Und Los")); Context.getInstance().defaultImpl = true; System.out.println("execute = " + main.mainService.get().execute("Und Los")); } } SubServiceA = 2014-11-21T15:01:51.129 SubSubServiceImpl => 2014-11-21T14:01:51.148Z execute = Und Los_A_SubSubServiceImpl Und Los SubServiceB = 2014-11-21T15:01:51.167 SubSubServiceImpl => 2014-11-21T14:01:51.168Z execute = Und Los_B_SubSubServiceImpl Und Los SubServiceA = 2014-11-21T15:01:51.168 SubSubServiceImpl => 2014-11-21T14:01:51.168Z execute = Und Los_A_SubSubServiceImpl Und Los
Besonderheiten von Dagger
Es gibt einige Besonderheiten bei Dagger, die, je nach Betrachtungsweise, ein Bug oder ein Feature sein können. Bis jetzt gingen wir einfach davon aus, dass es immer nur einen Objekt-Graphen geben wird – das war eine implizite Annahme. Was aber passiert, wenn wir n Instanzen des Objekt-Graphen erzeugen? Wenn wir zwei verschiedene ObjectGraph-Instanzen in der Klasse Main erzeugen lassen, bekommen wir immer eine neue Instanz. So weit, so gut. Allerdings gibt es bei Singletons auch immer eine neue Instanz. Das bedeutet, dass Singletons immer nur pro ObjectGraph-Instanz gültig sind.
@Provides @Singleton SubSubService provideSubSubService() { return new SubSubServiceImpl(); } //.. final ObjectGraph objectGraphA = ObjectGraph.create(new BusinessModule()); final ObjectGraph objectGraphB = ObjectGraph.create(new BusinessModule()); //Singletons gelten nur per ObjectGraph final Main mainA = objectGraphA.get(Main.class); final Main mainB = objectGraphB.get(Main.class); mainA.mainService.execute("Und Los"); mainB.mainService.execute("Und Los"); SubServiceA = 2014-11-21T15:10:58.054 SubSubServiceImpl => 2014-11-21T14:10:58.071Z SubServiceA = 2014-11-21T15:10:58.090 SubSubServiceImpl => 2014-11-21T14:10:58.090Z
Aber es gibt noch etwas zu beachten: Es besteht ja auch noch die Option, eine Instanz mittels der Methode inject(..) zu initialisieren. Der Container kontrolliert nicht, ob das bei einer Instanz schon einmal durchgeführt worden ist. Es gibt auch keine offizielle Prüfmethode, sondern man muss selber Attribute z. B. auf null prüfen. Ein wenig verwirrend wird es, wenn man das nun auch zwischen verschiedenen Objekt-Graphen macht.
Main main = new Main(); final ObjectGraph objectGraphA = ObjectGraph.create(new BusinessModule()); final ObjectGraph objectGraphB = ObjectGraph.create(new BusinessModule()); objectGraphA.inject(main); System.out.println("main.mainService = " + main.mainService); System.out.println("####### next inject #######"); objectGraphA.inject(main); System.out.println("main.mainService = " + main.mainService); //ohoh.. jetzt wird es durcheinander.... System.out.println("####### next object graph #######"); objectGraphB.inject(main); System.out.println("main.mainService = " + main.mainService); SubServiceA = 2014-11-21T15:19:46.629 SubSubServiceImpl => 2014-11-21T14:19:46.646Z main.mainService = org.rapidpm.demo.business.MainServiceImpl@17f052a3 ####### next inject ####### SubServiceA = 2014-11-21T15:19:46.664 main.mainService = org.rapidpm.demo.business.MainServiceImpl@2e0fa5d3 ####### next object graph ####### SubServiceA = 2014-11-21T15:19:46.666 SubSubServiceImpl => 2014-11-21T14:19:46.666Z main.mainService = org.rapidpm.demo.business.MainServiceImpl@5010be6
Fazit
Insgesamt ist der Ansatz von Dagger sehr angenehm, auch wenn das Framework hat einige Besonderheiten hat, die es zu beachten gilt. Die Performance bei der Ausführung ist sehr gut, die Verfügbarkeit für Java und Android sicherlich ein wichtiges Entscheidungskriterium. Bedauerlich ist allerdings, dass es sich wieder nur um ein DI- und nicht um ein CDI-(Contexts and Dependency Injection)-Framework handelt.
Hinterlasse einen Kommentar