Dokumentation zur Ausarbeitung des Artikels von Martin Fowler Inversion of Control Containers and the Dependency Injection pattern http://martinfowler.com/articles/injection.html von André Kley – 269767 Julian Päuler – 270140 11.12.2006 1.Martin Fowler 2.Einleitung 3.IoC / Dependency Injection 3.1 Constructor Injection 3.2 Setter Injection 3.3 Interface Injection 4.Service Locator 4.1 Nutzung eines abgetrennten Interfaces 4.2 Dynamischer Service Locator 4.3 Nutzung von Locator und Injection 5. Service Locator vs Dependency Injection 6. Constructor vs Setter Injection 7. Fazit 8. Eigene Beispiele 9. Quellen 1. Martin Fowler (Folie 3) Martin Fowler ist Autor und renommierter Referent zum Thema Softwarearchitektur, spezialisiert auf objekt-orientierte Analyse und Design, UML, Entwurfsmuster und agile Softwareentwicklung. Er schrieb fünf bedeutende Bücher zum Thema Softwareentwicklung. Heute arbeitet er als Chefentwickler beim Consulting-Unternehmen ThoughtWorks. 2. Einleitung (Folie 4/5) Ein generelles Problem in der Softwareentwicklung ist die Art der Zusammenführung unterschiedlicher Elemente: Wie bekommt man diese Web Controller Architektur mit einem Datenbank Interface unter einem Dach, wenn beide von unterschiedlichen Teams entwickelt wurden, die sich gegenseitig nicht kennen. class MovieLister... public Movie[] moviesDirectedBy(String arg) { List allMovies = finder.findAll(); for (Iterator it = allMovies.iterator(); it.hasNext();) { Movie movie = (Movie) it.next(); if (!movie.getDirector().equals(arg)) it.remove(); } return (Movie[]) allMovies.toArray(new Movie[allMovies.size()]); } Der Kern des Artikels ist dieses „finder“ Objekt oder die Art wie man das „lister“ Objekt mit diesem bestimmten „finder“ Objekt verbindet. Der Grund warum dies interessant ist, ist dass man die „moviesDirectedBy“ Methode komplett unabhängig von der Speicherung der Filme nutzen möchte. Somit ist alles was diese Methode macht ein Verweis auf einen „finder” zu legen und alles was der „finder” macht, ist zu wissen wie man auf die „findAll” Methode zu antworten hat. Dies kann man hervorheben durch die Definition eines Intefaces für den „finder“: public interface MovieFinder { List findAll(); } Nun ist alles sehr gut entkoppelt, aber letztendlich muss man eine konkrete Klasse einführen, die die Filme herausgibt: class MovieLister... private MovieFinder finder; public MovieLister() { finder = new ColonDelimitedMovieFinder("movies1.txt"); } Wenn man diese Klasse nur für sich nutzt, ist alles schön und gut. Wenn aber jemand anderes von dieser Funktion Gebrauch machen möchte, aber seine Filmlisten komplett anders abspeichert (z.b.: über eine SQL Datenbank, eine XML Datei, einen Web Dienst, oder einfach ein anderes Format von einer Textdatei), benötigt man eine andere Klasse die diese Daten erfasst. Da bereits eine „MovieFinder“ Schnittstelle definiert ist, braucht man die „moviesDirectedBy“ Methode nicht zu ändern. Aber man braucht immer noch eine Art und Weise wie man eine Instanz der richtigen „finder“ Implemenation integriert, die nur noch von der Schnittstelle abhängig ist. Man benötigt eine Implementationsklasse vom „finder“ die nicht beim Kompilieren eingebunden wird. Stattdessen soll der „lister“ mit allen jetzigen und zukünftigen Implementierungen zusammenarbeiten. Das Problem dabei ist, wie erstellt man diese Verknüpfung, so dass die „lister” Klasse die Implementierungsklasse ignoriert, aber dennoch mit den Instanzen kommunizieren kann. Man benötigt dafür Plugins, die Dienste gewährleisten, so dass man unterschiedliche Implemenationen in unterschiedlichen Verwendungen nutzen kann. Somit ist das Kernproblem, die Einbindung dieser Plugins in die Applikation. Dies wird gelöst durch die Umkehrung der Kontrolle („Inversion of Control”, IoC). Frei nach dem Motto: Don’t call us, we call you. (Hollywood Prinzip) 3. IoC / Dependency Injection (Folie 6) Frühere Benutzerschnittstellen wurden durch die Applikation kontrolliert. Man hatte dort eine Sequenz von Kommandos wie „Name eingeben”, „Adresse eingeben”. Das Programm führte durch die Eingaben und erwartete die entsprechenden Antworten. Durch grafische Benutzeroberflächen beinhaltete das Benutzerschnittstelle Framework diese Schleifen, das Programm hingegen liefert Ereignisabfragen für die verschiedenen Felder des Dialogs. Die Hauptkontrolle das Programms wurde umgekehrt, weg von einem, hin zum Framework. Für diese neue Art von Containern ist die Unkehrung, die Art wie sie eine Plugin Implementation suchen. In diesem Beispiel hat der „lister“ die „finder“ Implementation durch direktes Ansprechen gefunden. Dies unterscheidet den „finder“ vom Plugin. Der Ansatz den diese Container verfolgen ist die Sicherstellung, dass jeder Nutzer des Plugins bestimmten Konventionen folgt, die es einem separaten Assembler-Modul erlauben die Implementation in den „lister“ einzubinden. Als Resultat vieler Diskussionen mit IoC Vertretern hat man sich auf den Namen „Dependency Injection” (Unabhängige Injizierung) geeinigt, da der Name IoC viel zu allgemein gehalten ist. 3.1 Constructor Injection (Folie 7/8/9) In den aktuellen IoC Diskussionen wird es auch als Typ 3 bezeichnet. PicoContainer benutzen einen Konstruktor um zu entscheiden, wie sie eine „finder” Implementierung in die „lister” Klasse injizieren. Für diese Aufgabe muss die „movie lister” Klasse einen Konstruktor deklarieren, der alles beinhaltet was injiziert werden muss. class MovieLister... public MovieLister(MovieFinder finder) { this.finder = finder; } Der „finder” wird ebenfalls vom PicoContainer gesteuert und injiziert somit den Dateinamen der Textdatei mittels eines Containers. class ColonMovieFinder... public ColonMovieFinder(String filename) { this.filename = filename; } Der PicoContainer braucht dann nur noch die Anweisung welche Implementierungsklasse mit welcher Schnittstelle verbunden werden soll und welche Zeichenkette in den „finder” injiziert werden soll. private MutablePicoContainer configureContainer() { MutablePicoContainer pico = new DefaultPicoContainer(); Parameter[] finderParams = {new ConstantParameter("movies1.txt")}; pico.registerComponentImplementation(MovieFinder.class, ColonMovieFinder.class, finderParams); pico.registerComponentImplementation(MovieLister.class); return pico; } Dieser Konfigurations-Code wird typischerweise in einer anderen Klasse festgehalten. Für dieses Beispiel schreibt jeder der den „lister“ nutzen möchte, den entsprechenden Konfigurations-Code in eine eigene Setup Klasse. Man könnte die Konfigurationsinformationen natürlich auch in eine separate Datei speichern, die dann von einer Klasse eingelesen wird. Der PicoContainer bietet das nicht, aber ein es eng verwandtes Projekt namens NanoContainer, welches die entsprechenden Funktionen mitbringt, um eine XML Konfigurationsdatei zu nutzen. Solche NanoContainer parsen den XML und konfigurieren den hinterlegten PicoContainer. Die Philosophie des Projektes ist die Trennung des Konfigurationsdateiformates von dem zugrunde liegenden Mechanismus. PicoContainer unterstützen nicht nur die Constructor Injection, sondern auch die Setter Injection. Dennoch bevorzugen die Entwickler Constructor Injection. 3.2 Folie 10/11/12: Setter Injection mit Spring In den aktuellen IoC Diskussionen wird es auch als Typ 2 bezeichnet. Das Spring Framework ist ein weitreichendes Framework für die Java Enterprise Entwicklung. Es beinhaltet eine Abstraktionsschicht für Transaktionen, persistente Frameworks, Web-Anwendungsentwicklung und JDBC. Wie PicoContainer unterstützt es beides: Constructor und Setter Injection. Aber die Entwickler bevorzugen die Setter Injection. Um den „movie lister” die Injektion zu ermöglichen, definiert man eine setting-Methode (Setter). Ebenso definiert man einen Setter für den String des „finders“. class MovieLister... private MovieFinder finder; public void setFinder(MovieFinder finder) { this.finder = finder; } class ColonMovieFinder... public void setFilename(String filename) { this.filename = filename; } Der dritte Schritt ist die Konfiguration der Dateien. Spring unterstützt die Konfiguration durch XML Dateien und auch durch Code, aber XML ist der bevorzugte Weg <beans> <bean id="MovieLister" class="spring.MovieLister"> <property name="finder"> <ref local="MovieFinder"/> </property> </bean> <bean id="MovieFinder" class="spring.ColonMovieFinder"> <property name="filename"> <value>movies1.txt</value> </property> </bean> </beans> 3.3 Folie 13: Interface Injection In den aktuellen IoC Diskussionen wird es auch als Typ 1 bezeichnet. Die dritte Injection-Technik ist die Definierung und Nutzung von Interfaces (Schnittstellen) für die Injektion. Zu Beginn dieser Technik definiert man eine Schnittstelle über welche man die Injektion durchführt. Hier ist die Schnittstelle um den „movie finder“ in ein Objekt zu injizieren. public interface InjectFinder { void injectFinder(MovieFinder finder); } Diese Schnittstelle wird nun durch denjenigen definiert, der auch die „MovieFinder“ Schnittstelle liefert. Sie muss durch jede Klasse implementiert sein, die einen „finder“ nutzen will, wie z.B. auch der „lister”. class MovieLister implements InjectFinder... public void injectFinder(MovieFinder finder) { this.finder = finder; } Der gleiche Ansatz wird genutzt um einen Dateinamen in die „finder” Implementation zu injizieren: public interface InjectFinderFilename { void injectFilename (String filename); } class ColonMovieFinder implements MovieFinder, InjectFinderFilename... public void injectFilename(String filename) { this.filename = filename; } Nun noch Konfigurations-Code um die Implementierung zu integrieren. Zuerst wie gehabt die Komponenten registrieren. Danach ein neuer Schritt: Jedes Interface benötigt Code für die Injektion der abhängigen Objekte. Dies geschieht durch Registrierung der Injector Objekte mit den Containern. class Tester... private void registerComponents() { container.registerComponent("MovieLister", MovieLister.class); container.registerComponent("MovieFinder", ColonMovieFinder.class); } private void registerInjectors() { container.registerInjector(InjectFinder.class, container.lookup("MovieFinder")); container.registerInjector(InjectFinderFilename.class, new FinderFilenameInjector()); } Bei der Interface Injection muss für jede Art der Abhängigkeit eine Schnittstelle definiert werden. Diese Notwendigkeit entfällt bei den anderen Varianten. 4. Service Locator (Folie 14/15) Der Hauptvorteil einer “Dependency Injection” ist, dass sie die Abhängigkeit der „MovieLister“ Klasse von der konkreten „MovieFinder“ Implementation löst. Das erlaubt mir die Weitergabe und die Implementation in passende Implemenatationen anderer Umgebungen. Aber Injektion ist nicht der einzige Weg diese Abhängigkeit zu beseitigen, ein Weiterer ist die Nutzung eines Service Locator. Die Grundidee hinter einem Service Locator besteht in einem Objekt, dass alle Dienste beschafft, die die Applikationen benötigen könnte. Ein Service Locator für diese Applikation hätte eine Methode die einen „movie finder“ zurückgibt, wenn dieser benötigt wird. Natürlich löst dies nicht das Grundproblem, man muss weiterhin den Locator in den Lister bekommen, was zu den folgenden Abhängigkeiten führt: In diesem Beispiel ist der Service Locator eine einfache Registry. Der „lister“ kann diese nutzen, um den „finder“ zu bekommen , sobald er instanziert wird: class MovieLister... MovieFinder finder = ServiceLocator.movieFinder(); class ServiceLocator... public static MovieFinder movieFinder() { return soleInstance.movieFinder; } public static void load(ServiceLocator arg) { soleInstance = arg; } public ServiceLocator(MovieFinder movieFinder) { this.movieFinder = movieFinder; } private static ServiceLocator soleInstance; private MovieFinder movieFinder; Der Service Locator muss dann nur noch konfiguriert werden: private void configure() { ServiceLocator.load(new ServiceLocator(new ColonMovieFinder("movies1.txt"))); } 4.1 Nutzung eines abgetrennten Interfaces für den Locator (Folie 16) Eins der Probleme des obigen Ansatzes ist, dass der „MovieLister“ abhängig von der kompletten ServiceLocator Klasse ist, obwohl er nur einen einzigen Dienst nutzt. Man kann dieses Problem reduzieren, indem man ein Interface zwischen dem MovieLister und dem Service Locator schiebt. Somit kann der „lister“ seinen Teil vom Service Locator ansprechen, ohne ihn komplett nutzen zu müssen. In diesem Fall stellt der Provider des „listers“ auch eine Locator Schnittstelle bereit, die er benötigt um den „finder“ zu bekommen. Der Locator muss dann diese Schnittstelle implementieren um Zugang zu dem „finder“ bereitzustellen. 4.2 Dynamischer Service Locator (Folie 17) Das obige Beispiel war statisch, da die ServiceLocator Klasse Methoden für jeden Dienst besaß den sie benötigte. Das ist aber nicht der einzige Weg dies zu tun. Man kann ebenfalls einen dynamischen Service Locator erstellen, der einem erlaubt jeden Dienst, den man benötigen könnte, zu integrieren und während der Laufzeit auszuwählen. Dazu benutzt der ServiceLocator eine HashMap, die die Services enthält. Sie bietet generische Methoden zum Erhalten und Laden der Dienste an. Das Konfigurieren umfasst dann das Laden eines Dienstes mit einem entsprechenden Schlüssel. Im Großen und Ganzen ist dieser Ansatz sicherlich flexibel aber nicht sehr explizit. Die einzige Art herauszufinden wie man einen Service erreicht ist die Nutzung von Textschlüsseln. Bei expliziten Methoden ist es einfacher diese zu finden, nämlich durch Nachschauen in den Schnittstellendefinitionen. 4.3 Nutzung von Locator und Injection (Folie 18) „Dependency Injection” und Sercice Locator sind keine sich gegenseitig ausschließende Konzepte. Ein gutes Beispiel beide zusammen zu nutzen ist das Avalon Framework. Avalon nutzt einen Service Locator, aber benutzt Injektion um den Komponenten mitzuteilen wo sie den Locator finden können. public class MyMovieLister implements MovieLister, Serviceable { private MovieFinder finder; public void service( ServiceManager manager ) throws ServiceException { finder = (MovieFinder)manager.lookup("finder"); } Die Service-Methode ist ein Beispiel für die Interface Injection, die dem Container erlaubt einen Service Manager in „MyMovieLister“ zu injizieren. Der ServiceManager ist ein Beispiel für einen Service Locator. Hier speichert der Lister den Manager nicht in einem Feld, stattdessen benutzt er ihn um den finder direkt zu speichern. 5. Service Locator vs Dependency (Folie 19) Beide Implementationen stellen das fundamentale Entkoppeln bereit und bei beiden ist der Applikationscode unabhängig von der konkreten Implementation des Service Interfaces. Service Locator: - die Applikations Klasse fragt explizit den Locator. - jeder Nutzer ist Abhängig vom Locator - um die Abhängigkeiten zu sehen, muss man den Code durchsuchen Dependency Injection: - es gibt keine explizite Anfrage, der Service erscheint in der Applikations Klasse – durch Inversion of Control. - schwieriger zu debuggen (Erfahrungswert von M.F.) - man sieht die Abhängigkeiten einfacher (Im Konstrukor oder bei den Set-Methoden) Wenn man nun verschieden Klassen entwirft, die auf einen Service zugreifen sollen, ist es nicht schwer den Service Locator zu benutzen. Wenn man im Beispiel den „MoveLister“ weitergeben möchte, ist das kein Problem. Man muss dann nur den Locator so konfigurieren, dass man die richtige Service Implementation wählt. (Per Code oder Datei) Hierbei ist Dependency Injection nicht wirklich notwendig. Der Unterschied ist, wenn man seine „lister“ Klasse an eine Anwendung anbinden möchte, die von anderen geschrieben wurde. Denn dann kennt man die API des Service Locator nicht. Jeder Kunde könnte seinen eigenen nicht kompatiblen Service Locator besitzen. Man könnte um das Problem herum kommen, indem man ein abgetrenntes Interface nutzt. Jeder Kunde müsste dann einen Adapter schreiben, der das Interface an seinen Locator bindet. Aber auf jeden Fall muss ich zuerst den Service Locator sehen um meine spezifiziertes Interface zu binden. Dadurch beginnt die Einfachheit der direkten Verbindung zu einem Locator zu schwinden. 6. Constructor vs Setter (Folie 20) Sowohl mit der Constructor Injection als auch mit der Setter Injection ist es sehr einfach Bindungen zwischen Komponenten herzustellen. Man muss nichts kurioses in den Komponenten machen und es ist einfach für den Injector alles zu konfigurieren. Bei der Interface Injection hingegen muss man sehr viele Interfaces schreiben. Sie ist dadurch zwar flexibler aber wird nur bei ganz wenigen Containern angewandt (Avalon). Gerade wegen diesem großen Aufwand verwenden heutzutage die meisten lightweight Container Setter oder Constructor Injection. Constructor Injection: - jedes Objekt ist sofort verfügbar - geringfügig weniger Code - unveränderbare Felder können versteckt werden Setter: - keine langen Argumentenlisten die unübersichtlich werden könnten - Setter bekommen eindeutige Namen (Bei Konstruktoren müssen die Variablen an der richtigen Position stehen) - Konstruktoren werden nicht automatisch vererbt 7. Fazit (Folie 21) Beim entwicklen von Anwendungsklassen sind Service Locator und Dependency Injection fast gleich. M.F. empfiehlt den Service Locator zu nutzen, es sei denn man entwickelt mehrere Klassen die in verschiedene Anwendungen laufen sollen. Nutzt man Dependency Injection empfiehlt es sich mit der Constructor-Injection zu starten. Man sollte aber bereit sein auf Setter Injection umzustellen, falls es zu oben genannten Problemen kommt. 8. Eigene Beispiele Beispiele zur Dipendency Injection Voraussetzung: Java SDK/JDK muss installiert sein Constructor Injection mit PicoContainern • • • Download der picocontainer.jar auf http://www.picocontainer.org/Downloads Diese .jar Datei muss in den Java Installation Pfad kopiert werden, unter ...\lib\ext Wie folgt müssen jetzt die Java Klassen erstellt werden. Girl.java public class Girl { Kissable kissable; public Girl(Kissable kissable) { this.kissable = kissable; } public void kissSomeone() { kissable.kiss(this); } } Diese Klasse wird die kiss Methode bei dem im Konstruktor regestriertem Interface aufrufen. Kissable.java public interface Kissable { void kiss(Object kisser); } Boy.java public class Boy implements Kissable { public void kiss(Object kisser) { System.out.println("I'm a Boy and I was kissed by a " + kisser); } } OldWoman.java public class OldWoman implements Kissable { public void kiss(Object kisser) { System.out.println("I'm an old woman and I was kissed by a " + kisser); } } Die beiden Klassen Boy und OldWoman können jeweils am Interface registriert werden (siehe unten). testWithPico.java import org.picocontainer.MutablePicoContainer; import org.picocontainer.defaults.*; public class testWithPico{ // Zur Testausführung public testWithPico(){ MutablePicoContainer pico = configureContainer(); Girl girl = (Girl) pico.getComponentInstance(Girl.class); girl.kissSomeone(); } // Anmelden der benötigten Klassen private MutablePicoContainer configureContainer() { MutablePicoContainer pico = new DefaultPicoContainer(); //pico.registerComponentImplementation(Kissable.class, Boy.class); pico.registerComponentImplementation(Kissable.class, OldWoman.class); pico.registerComponentImplementation(Girl.class); return pico; } public static void main(String args[]) { testWithPico lala = new testWithPico(); } } Durch den Befehl registerComponentImplementation wird die jeweilige Klasse an dem Interface registriert, bzw. die Girl Klasse wird hier am PicoContainer registriert und bekommt später beim anlegen das Kissable Objekt per Konstruktor zugewiesen. Je nachdem ob die Boy oder OldWoman Klasse registriert wird sieht dies dann so aus: System ausgabe: I'm a Boy and I was kissed by a Girl System ausgabe: I'm an old woman and I was kissed by a Girl Setter Injection mit Spring • • • Download des Spring Frameworks auf http://www.springframework.org/download Die .jar Dateien o spring.jar aus spring-framework...\dist und die o commons-logging.jar aus spring-framework...\lib\jakarta-commons o müssen in den Java Installation Pfad kopiert werden, unter ...\lib\ext. Wie folgt müssen jetzt die Java Klassen erstellt werden. Girl.java public class Girl { Kissable kissable; public void setKissable(Kissable kissable) { this.kissable = kissable; } public void kissSomeone() { kissable.kiss(this); } } Anstelle des Konstruktor wird diese Klasse nun per Set Methode registriert. TestWithSpring.java import org.springframework.context.ApplicationContext; import org.springframework.context.support.FileSystemXmlApplicationContext; public class testWithSpring{ // Zur Testausführung public testWithSpring(){ ApplicationContext ctx = new FileSystemXmlApplicationContext("conf.xml"); Girl girl = (Girl) ctx.getBean("Girl_id"); girl.kissSomeone(); } public static void main(String args[]) { testWithSpring lala = new testWithSpring(); } } An den restlichen Klassen ändert sich nichts. Die Konfiguration wird nun per XML Datei definiert. conf.xml <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd"> <beans> <bean id="Girl_id" class="Girl"> <property name="kissable"> <ref local="Kissable_id"/> </property> </bean> <bean id="Kissable_id" class="Boy"> // hier OldWoman oder Boy </bean> </beans> Die Girl Klasse wird unter der „id“ Girl_id registriert, die Boy Klasse unter der „id“ Kissable_id. Die Girl Klasse hat Set Methode mit dem Namen „SetKissable“ daher muss hier durch „property name“ auch „kissable“ angegeben werden, dieses Methode bekommt ein Objekt vom Typ Kissable_id (also der Boy Klasse). Die TestWithSpring.java ausführen, ausgabe wie oben. Interface Injection Die Java Klassen müssten wie folgt abgeändert werden bzw. ein Interface würde hinzu kommen. Diese Art der Injection ist anscheinend nicht sehr beliebt, bis auf das Hivemind Framework http://hivemind.apache.org/ das die Interface Injection unterstützen soll, ist uns kein weiteres Framework bekannt. Girl.java public class Girl implements InjectKissable{ Kissable kissable; public void injectKissable(Kissable kissable){ this.kissable = kissable; } public void kissSomeone() { kissable.kiss(this); } } InjectKissable.java public interface InjectKissable { void injectKissable(Kissable kissable); } Service Locator Wie folgt müssen die Java Klassen erstellt werden. Girl.java public class Girl { Kissable kissable = ServiceLocator.getKissable(); public void kissSomeone() { kissable.kiss(this); } } ServiceLocator.java public class ServiceLocator{ public ServiceLocator(Kissable kissable) { this.kissable = kissable; } public static Kissable getKissable() { return soleInstance.kissable; } public static void load(ServiceLocator arg) { soleInstance = arg; } private static ServiceLocator soleInstance; private Kissable kissable; } testWithSL.java public class testWithSL{ // Zur Testausführung public testWithSL(){ configure(); Girl girl = new Girl(); girl.kissSomeone(); } // Anmelden der benötigten Klassen private void configure() { ServiceLocator.load(new ServiceLocator(new Boy())); } public static void main(String args[]) { testWithSL lala = new testWithSL(); } } Die restlichen Klassen bleiben unverändert. In der configure Methode wird die Boy Klasse am ServiceLocator angemeldet. Die TestWithSL.java ausführen, ausgabe wie oben. 9. Quellen http://martinfowler.com/articles/injection.html http://www.springframework.org http://www.picocontainer.org