Plug-ins programmieren mit Services und Dependency Injection

Das e4-Programmiermodell

Eines der Ziele des e4-Projekts ist es, die Programmierung von Eclipse-Plug-ins zu vereinfachen. Eine Schlüsselstellung dabei nimmt das Programmiermodell ein, das sich zwar noch im Fluss befindet, dessen Hauptbestandteile und Hauptrichtung aber schon jetzt feststehen.

Die Programmierung von Eclipse-Plug-ins einfacher zu machen, ist eine anspruchsvolle Aufgabe. Nicht deshalb, weil es einfacher nicht mehr geht, sondern weil Vereinfachung auch immer bedeutet, etwas wegzulassen. Dabei gilt es, sorgfältig abzuwägen, was noch zum Kern der Plattform gehören soll u

nd wo die Spezialfälle beginnen, durch die das API über Gebühr komplizierter wird. Zunächst muss man zwischen einem API unterscheiden, das rein innerhalb einer bestimmten Komponente (zum Beispiel innerhalb einer View oder eines Editors) verwendet wird, und einem API, mit dem die Komponente sich

in Eclipse integriert. Mit dem Begriff „e4-Programmiermodell“ ist nur Letzteres gemeint. Das erste Problem anzugehen, also wie man z. B. die SWT- und JFace-Programmierung vereinfachen kann, ist Ziel anderer Technologien im e4-Projekt, zum Beispiel XWT oder TM.

Bisher ist das API, über das sich eine Komponente in Eclipse integriert, größtenteils so aufgebaut, dass man ausgehend von einem relativ allgemeinen Objekt (die Workbench-Instanz, die Workspace-Instanz oder die IDE-Klasse) die gewünschte Funktionalität finden kann – manchmal etwas versteckt durch Zwischenobjekte wie das aktive Workbench-Fenster oder die aktuelle Perspektive. Dies führt zu zwei Problemen:

  1. Durch die Verwendung des Singleton-Patterns entsteht eine relativ enge Kopplung zwischen allen Stellen im Code, die dasselbe Objekt referenzieren. Der Kasten „Singleton – Wolf im Schafspelz“ erklärt im Detail die Probleme, die das Singleton-Pattern verursacht.
  2. Im Laufe der Zeit sammelt sich in diesen allgemeinen Objekten immer mehr Funktionalität an, und die ursprünglichen Verantwortlichkeiten der beteiligten Klassen sind nicht mehr klar zu erkennen. So hat zum Beispiel das Interface IWorkbench (Abb. 1) inzwischen 23 Methoden, die verschiedene Services zurückliefern – Tendenz steigend. Diese Services hängen zwar irgendwie logisch mit der Workbench zusammen, können aber kaum als echte Verantwortlichkeiten der Workbench verargumentiert werden. Andere Interfaces, wie z. B. IWorkbenchWindow, IWorkbenchPage etc., stellen weitere Services zur Verfügung.

Abb. 1: „Monster-Interface“ IWorkbench

[ header = Seite 2: Singleton-Pattern ]

Abstrakt betrachtet, fungieren diese Interfaces neben ihrer eigentlichen Aufgabe als relativ primitive „Service Broker“, über die die verschiedenen Services verfügbar gemacht werden. Primitiv deswegen, weil im Gegensatz zur kanonischen Servicearchitektur (Abb. 2) neue Services dadurch hinzukommen, dass neue Methoden im Interface hinzugefügt werden. Dies sorgt dafür, dass das Interface nur immer größer werden kann und niemand außer der Workbench selbst Services registrieren kann.

Abb. 2: Servicearchitektur

Im e4-Programmiermodell hat Code, der eine e4-Komponente wie zum Beispiel eine View oder einen Editor implementiert, weder direkte Abhängigkeiten zu einem Service Broker noch zu Objekten, die den Zustand der Workbench repräsentieren. Die Abhängigkeiten existieren stattdessen nur auf der Ebene einzelner Services. Wie sieht nun konkret eine Klasse aus, die als View eingebunden werden kann? Zum Beispiel wie folgt:

public class ExifTable { @Inject Composite parent; @Inject Logger logger; @Inject void setInput(IResource resource) { ... } @Inject IMemento persistedState; @PostConstruct void init() { // create UI ... } } 

Der obige Code ist eine vereinfachte Version der Klasse ExifTable aus der e4 „Photo Demo“. Die Klasse implementiert eine View, die EXIF-Daten wie zum Beispiel Verschlusszeit und Blendenzahl für die selektierte Bilddatei oder den selektierten Ordner mit Bilddateien anzeigt. Dazu werden verschiedene Objekte benötigt: ein Parent-Composite, um die Tabellen-Widgets erzeugen zu können, ein Logger-Objekt für Debugging-Zwecke, die momentan selektierte Datei oder der momentan selektierte Ordner, und ein IMemento-Objekt zum Persistieren von UI-Zustand wie zum Beispiel Spaltenbreiten in der Tabelle. Wenn eine Instanz der ExifTable-Klasse instanziiert wird, werden diese Objekte vom Dependency-Injection-Mechanismus zur Verfügung gestellt. Dazu werden zunächst die Instanzvariablen gesetzt, danach die Setter-Methoden und schließlich die init-Methode aufgerufen. Im Vergleich zu einer „normalen“ View-Implementierung fällt auf:

  • Die Klasse erbt nicht von einer Frameworkklasse wie ViewPart. Das macht sie auch in anderen Kontexten verwendbar, zum Beispiel in einem Dialog oder als Teilkomponente eines Editors.
  • Services und andere benötigte Objekte, wie zum Beispiel das Parent-Composite, sind mit Java-5-Annotationen gekennzeichnet. Sie werden von einem Dependency-Injection-Mechanismus bereitgestellt – siehe auch den Kasten zum Thema „Dependency Injection“. Dadurch gibt es keine Codeabhängigkeiten mehr zu so etwas wie einem Service Broker.
Singleton-Pattern: Wolf im Schafspelz
Gäbe es ein Buch „Design Patterns 2.0“, würde das Singleton-Pattern nicht wieder aufgenommen [2]. Aber warum? Das Pattern funktioniert doch und erfüllt seine Aufgabe. Man könnte sich fragen: Was ist denn das Problem am Singleton-Pattern?
Durch die Verwendung von globalem Zustand, also von statischen Variablen, sind die Komponenten, die auf diesen globalen Zustand zugreifen, nicht mehr frei kombinierbar. Singletons sind zwar nicht ganz so „schlimm“ wie statische Variablen, die gelesen und geschrieben werden können, aber sie führen trotzdem zu einer engen Kopplung. Die Kopplung besteht nicht nur zwischen dem Bundle, das das Singleton definiert, und dem Bundle, das auf das Singleton zugreift. Vielmehr sind auf einmal alle Bundles, die auf dasselbe Singleton zugreifen, miteinander gekoppelt, weil sie nur auf eine Instanz der Singleton-Klasse zugreifen können.
Konkret führt das nicht unbedingt sofort zu Problemen. Während der Entwicklung von Eclipse 1.0 waren zunächst keine Singletons verwendet worden, etwa für den Zugriff auf den Workspace (ResourcesPlugin.getWorkspace). Das bedeutete, dass der aktuell gemeinte Workspace immer als Argument von Methode zu Methode durchgereicht werden musste. Auf die Dauer wurde das so umständlich, dass die Entwickler sich entschieden, die Referenz zum Workspace als Singleton zur Verfügung zu stellen. Die Konsequenz war zunächst, dass die Entwickler es bei der Weiterentwicklung von Eclipse einfacher hatten. Erst viel später wurden die Nachteile klar: Durch die Verwendung des Workspace-Singletons ist der gesamte existierende Code, der darauf zugreift, auf einen einzigen Workspace beschränkt. Zum Beispiel kann man deswegen den inkrementellen Eclipse-Compiler innerhalb einer Java VM nur einmal laufen lassen, für einen Benutzer. Wenn man Eclipse in ein serverseitiges Backend und ein User Interface auf Clientseite aufteilen wollte, könnte also der Server nicht mehrere Benutzer gleichzeitig unterstützen.
Ähnliche Beispiele gibt es im Eclipse UI: Der Compare-Editor zeigt zwei Versionen einer Datei nebeneinander an. Wenn es sich um eine Java-Datei handelt, würde man gerne alle Features des Java-Editors verwenden können, z. B. Tastenkürzel, das bekannte Kontextmenü oder Navigationsmöglichkeiten per CTRL-Klick oder F3. All dies war sehr lange Zeit im Compare-Editor nicht vorhanden, weil der Java-Editor nur auf „oberster Ebene“ funktioniert, also nicht in einen anderen Kontext eingebettet werden kann. Erst in Eclipse 3.5 hat das Compare-Team in Zusammenarbeit mit dem JDT-Team viele der gewünschten Features zum Laufen gebracht, mit relativ hohem Aufwand.

[ header = Seite 3: Fortsetzung Singleton-Pattern ]

Hinter den Kulissen existiert natürlich ein Service-Broker-Äquivalent, aber in der Regel bleibt dies als Implementierungsdetail verborgen. Zu jedem nennenswerten Objekt im Workbench-Modell, wie etwa der Window, der Perspective oder dem Part, gibt es einen so genannten IEclipseContext. Der Kontext ist in erster Linie ein Mechanismus mit der Aufgabe, zu einer Serviceanforderung eine passende Serviceinstanz zu finden. Dabei sind Kontextobjekte in einer Hierarchie eingeordnet und haben lokal gespeicherte Serviceobjekte, die unter ihrem Namen abgelegt sind. Wird ein Service im lokalen Kontext nicht gefunden, wird so lange in den jeweils übergeordneten Kontexten gesucht, bis ein Serviceobjekt gefunden werden kann. Ausgehend von einer View gibt es einen lokalen Kontext, über den zum Beispiel ein IMemento gefunden werden kann, in dem der UI-Zustand der Komponente der letzten Sitzung verfügbar ist. Der nächste übergeordnete Kontext speichert Services und Objekte, die spezifisch für die Perspektive sind, in der die View erscheint. Darüber existiert ein Kontext für das Fenster, in dem zum Beispiel die fensterbezogene aktuelle Selektion verfügbar ist. Wiederum übergeordnet gibt es einen anwendungsweiten Kontext. Dieser Kontext stellt die Schnittstelle zu den OSGi-Services dar – alle zur jeweiligen Zeit verfügbaren OSGi-Services werden in diesem Kontext bereitgehalten.

Eine vorläufige Liste von Services, die in e4 zur Verfügung stehen sollen, befindet sich in Tabelle 1. Die Zerteilung des APIs in einzelne Services hat den Vorteil, dass in Zukunft einzelne Services aktualisiert werden oder sogar, falls nötig, durch neue Versionen ersetzt werden können. Außerdem ist es einfach, Basisservices von eher fortgeschrittenen Services zu unterscheiden und zu trennen. Dies macht die Plattform insgesamt beweglicher.

Service

JBeschreibung

Life Cycle

Ein definiertes Protokoll für Erzeugung/Initialisierung und Abmeldung/Entsorgung von Komponenten

Authentifizierung

Zur Anforderung der Authentifizierung des aktuellen Benutzers

Fehlerbehandlung

Konsistente Art und Weise, in der mit unerwarteten Fehlern und Zuständen umgegangen wird

Logging und Tracing

Hilfsmittel zum Debugging von seltenen Ereignissen oder für komplexe Event-Abfolgen

Extension Registry

Registrierung von sowie Zugriff auf Extensions und Extension Points

Langablaufende Operationen

Einrichten von Prozessen für Hintergrundverarbeitung, Berichten über Fortschritt von langablaufenden Operationen, Abbruchmöglichkeit

Einstellungen

Lese- bzw. Schreibzugriff auf Benutzereinstellungen

Sprachanpassung von Meldungen

Dienste für die Lokalisierung von Meldungen etc.

Objekte adaptieren

Unterstützung für das Adapter-Pattern

Commands/Handlers

Definition von abstrakten Aktionen sowie deren Implementierung

Ereignismanagement

Vereinfachte Möglichkeit, auf alle Arten von Ereignissen zu reagieren

Undo/Redo

Verwaltung der Undo- und Redo-Funktionalität

Selektion

Verwaltung der aktuellen Selektion und Reagieren auf dessen Veränderung

Persistieren von UI-Zustand

Mechanismen, um den Zustand der Benutzerschnittstelle zu persistieren

Verwaltung von gemeinsamen Ressourcen

Unterstützung für die Verwaltung gemeinsam benutzter Ressourcen (z. B. Fonts, Farben etc.)

Editor Life Cycle

Verwaltung von Komponenten (z. B. Editoren) mit ungespeicherten Änderungen

Notifikation

Unterstützung für temporäre Notifikation des Benutzers über wichtige Ereignisse

Tabelle 1: Vorläufige Liste der wichtigsten Eclipse Application Services

An dieser Stelle sollen auch Nachteile des Programmiermodells nicht verschwiegen werden. So ist es zum Beispiel nicht mehr möglich, sich mithilfe von „Code Completion“ bei der Suche leiten zu lassen, welche Services zur Verfügung gestellt werden. Das bedeutet, dass die Dokumentation eine wichtigere Rolle spielen wird. Unser Ziel ist es, dass die wichtigsten 20 Services, die den 80 %-Fall abdecken sollen, jeweils auf ca. einer DIN-A4-Seite beschrieben werden können.

Bis zum 4.0 Release des Eclipse SDKs ist noch eine Menge zu tun und viele Details werden sich noch ändern. Ich hoffe aber, dass dieser Artikel einen ersten Einblick in die grundlegenden Konzepte von e4 liefern konnte. Das Programmiermodell erfordert einen anderen Stil und ist sicherlich zunächst noch etwas ungewohnt. Einige e4-Committer haben aber schon zurückgemeldet, dass ihnen das neue Programmiermodell dabei hilft, klareren Code zu schreiben und mit weniger Aufwand mehr zu erreichen. Daran hat nicht nur die Verwendung von Dependency Injection Anteil, sondern auch die Integration der OSGi-Services, die „einfach so“ als Services zur Verfügung stehen. Mehr Informationen sind auf den Wiki-Seiten des e4-Projekts zu finden [1].

[ header = Seite 4: Dependency Injection ]

Dependency Injection
Warum sich der relativ furchterregende Name „Dependency Injection“ durchgesetzt hat, möchte ich auch gerne wissen. Hinter dem Namen verbirgt sich die Idee, dass man strikt zwischen Objekterzeugung und regulärem Code unterscheidet und die Objekterzeugung einem so genannten Injector überlässt.
Mit anderen Worten wird dernew-Operator verbannt – wie soll das gehen und warum sollte man das machen? Die Argumentation ist ähnlich wie beim Singleton-Pattern: Jedes Mal, wenn man dennew-Operator verwendet, bedeutet das eine enge Kopplung zwischen dem Code, der die Klasse instanziiert, und der Klasse, die instanziiert wird. Die Existenz des Factory-Patterns beweist, dass häufig mehr Flexibilität gebraucht wird, und sei es nur, um Code einfacher testbar zu machen. Um in Unit-Tests eine Klasse (Unit) in Isolation zu testen, muss man die Objekte, mit denen das getestete Objekt interagiert, entsprechend ersetzen.
In der Literatur werden die Ersatzobjekte allgemein „Doubles“ genannt, als Oberbegriff der konkreteren Dummy-, Fake-, Stub- oder Mock-Objekte [3]. Wenn allerdings die zu testende Klasse die Objekte, mit denen sie interagiert, selbst mit Hilfe des new-Operators erzeugt, oder die Objekte über statische Methoden direkt referenziert, kann man ihr keine Doubles unterjubeln. Als Beispiel kann die folgende Klasse dienen, grob angelehnt an eine existierende Eclipse-Klasse:
public class ResourceNavigator extends ViewPart { private IMemento memento; private FrameList frameList = new FrameList(new FrameSource()); private ResourcePatternFilter patternFilter = new ResourcePatternFilter(); public void init(IViewSite site, IMemento memento) { super.init(site, memento); this.memento = memento; } } 
Die Lösung dieses Problems besteht darin, dass jedes Objekt im Rahmen seiner Initialisierung die anderen Objekte, mit denen es interagieren soll, von außen zur Verfügung gestellt bekommt. Das können Argumente des Konstruktors sein, aber auch schreibbare Instanzvariablen oder Setter-Methoden. Auf diese Weise werden alle Objekte entsprechend parametrisiert, und der Code, der die Objekte – besser gesagt ganze Objektgraphen – erzeugt, kann separat gehalten werden. Also würde man die obige Klasse zum Beispiel wie folgt umschreiben:
public class ResourceNavigator extends ViewPart { private IMemento memento; private FrameList frameList; private ResourcePatternFilter patternFilter; public ResourceNavigator(FrameList frameList, ResourcePatternFilter patternFilter) { this.frameList = frameList; this.patternFilter = patternFilter; } public void init(IViewSite site, IMemento memento) { super.init(site, memento); this.memento = memento; } } 
Der separate Code zum Konfigurieren des Objektgraphen sähe dann so aus:
IFrameSource frameSource = new FrameSource(); FrameList frameList = new FrameList(frameSource); ResourcePatternFilter patternFilter = new ResourcePatternFilter(); ResourceNavigator resourceNavigator = new ResourceNavigator(frameList, patternFilter); 
Bis hierhin hatten wir es mit reinen Umformungen des Quelltexts zu tun, um Objektgraphen extern konfigurieren zu können, anstatt die Konfiguration den Objekten selbst zu überlassen. Ein Dependency-Injection-Framework dient nun dazu, den relativ langweiligen Code zum Erzeugen des Objektgraphen nicht selbst schreiben zu müssen. Im Fall von e4 würde man etwa Folgendes schreiben (andere Dependency-Injection-Frameworks wie z. B. Google Guice erfordern sehr ähnlichen Konfigurationscode):
Injector injector = new Injector(); injector.bind(IFrameSource.class, FrameSource.class); ResourceNavigator navigator = injector.getInstance(ResourceNavigator.class); 
Zur Erzeugung des Objektgraphen verwendet der Injector Reflection, beginnend mit der gewünschten Klasse ResourceNavigator, und findet eine Liste von benötigten Objekten, in unserem Fall über den Konstruktor. Diese Objekte wiederum haben möglicherweise selbst Abhängigkeiten zu weiteren Objekten, zum Beispiel benötigtFrameList eine Instanz von FrameSource. Der Objektgraph wird Bottom-Up aufgebaut, ähnlich zu dem Codebeispiel weiter oben, in dem die Objekte explizit instanziiert wurden. Allerdings muss man über Annotationen noch mitteilen, welche Konstruktoren, Setter-Methoden und Instanzvariablen vom Framework aufgerufen oder gesetzt werden sollen. Andererseits bietet dies die Möglichkeit, den Code weiter zu vereinfachen, da der Injector auch Instanzvariablen direkt setzen kann:
public class ResourceNavigator extends ViewPart { private IMemento memento; @Inject FrameList frameList; @Inject ResourcePatternFilter patternFilter; public void init(IViewSite site, IMemento memento) { super.init(site, memento); this.memento = memento; } } 
JSR-330
Vor kurzem (im Oktober 2009) wurde der JSR-330 verabschiedet, in dem Annotationen für Dependency Injection standardisiert werden. Nicht nur Google Guice und Spring als die beiden am weitesten verbreiteten Dependency-Injection-Frameworks unterstützen in den jeweilig neuesten Versionen diese Annotationen. Auch der im e4-Projekt entwickelte Injector basiert auf den Annotationen des JSR-330. Es handelt sich um eine relativ kleine Anzahl von Konzepten, die hier knapp erklärt werden sollen – für eine ausführlichere Erklärung sei auf [4] verwiesen:
• javax.inject.Inject: Diese Annotation kennzeichnet Konstruktoren, Instanzvariablen und Setter-Methoden, die im Rahmen der Objektinitialisierung mit entsprechenden Objekten aufgerufen werden.
• javax.inject.Qualifier und javax.inject.Named: Ein Qualifier kann dazu verwendet werden, ein zu injizierendes Objekt näher zu spezifizieren. Ein Standard-Qualifier ist Named, mit dem Objekte per Namen identifiziert werden können. Weitere Qualifier kann man selbst definieren.
• javax.inject.Scope und javax.inject.Singleton: Ein Scope definiert, wie Instanzen vom Injector beim Initialisieren von Objekten wiederverwendet werden sollen. Wenn eine Klasse mit Singleton annotiert ist, bedeutet das, dass der Injector nur eine Instanz der Klasse erzeugen und diese Instanz immer wieder verwenden soll. Weitere Scopes kann man selbst definieren.
• javax.inject.Provider: Ein Provider entspricht einer vom Injector bereitgestellten Factory-Instanz. Das Interface Provider definiert genau eine parameterlose Methode get(), die ein Objekt vom Typ T zurückliefert. Mithilfe dieses Interfaces kann man das Erzeugen des gewünschten Objekts verzögern, bis es tatsächlich gebraucht wird.

Boris Bokowski arbeitet für IBM Canada in Ottawa. Er leitet das Eclipse Platform UI Team und hat entscheidenden Anteil an der Entwicklung des e4-Projekts. Er hat an der TU Darmstadt studiert und an der FU Berlin promoviert.
Geschrieben von
Kommentare

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht.