Google Guice: Gut getestet, modular und wartbar

Eclipse-Plug-in-Entwicklung und Guice

Für die Integration mit anderen Eclipse-Plug-ins stellt Eclipse im Prinzip zwei Möglichkeiten zur Verfügung. Man kann einerseits auf Code in importierten Bundles (oder Packages) direkt zugreifen. Dabei möchte ich noch zwischen Zugriff auf Objekte und Zugriff auf Code unterscheiden.

Für Guice ist es im Prinzip egal ob eine zu instanzierende Klasse oder der Typ einer Abhängigkeit aus einem anderen Bundle kommt. Da Guice über Klassenliterale konfiguriert wird und auch deklarierte Abhängigkeiten im Kontext einer Klasse bestehen, hat Guice zu jeder Zeit den richtigen Classloader zur Hand.

Singletons

Möchte ich aus meinem Bundle auf einen Zustand aus einem anderen Bundle zugreifen, so geschieht das immer über statische Java Properties. Als Einstiegspunkt wird hier oft der Activator verwendet, an dem dann eine statische Methode deklariert ist, die mir die Instanz dazu gibt. Von da aus hangelt sich der Eclipse-Entwickler dann zu dem Objekt, das gerade wirklich benutzt wird. Um z. B. an den IWorkspaceRoot zu kommen schreibe ich:

ResourcesPlugin.getWorkspace().getRoot()

Wenn Sie diesen Ausdruck irgendwo in Ihre Klasse schreiben, werden Sie die Klasse nicht mehr testen können, ohne den Testcase eines Workspaces zu erzeugen. Der Zustand des Workspaces muss natürlich für den Testfall aufgesetzt werden und sollte hinterher auch wieder so aussehen wie vor dem Test. Das kann (und muss) man lösen, die Tests werden aber extrem langsam und relativ aufwendig. Wenn ich Guice verwende, lasse ich mir das IWorkspaceRoot-Objekt einfach injizieren.

@Inject 
public MyComponent(IWorkspaceRoot workspaceRoot) { 
  ...
}

Ich muss dann nur noch in meinem Produktiv-Module folgendes Binding eintragen:

bind(IWorkspaceRoot.class).toInstance(ResourcesPlugin.getWorkspace().getRoot());

Im Unit Test könnte ich die Komponente dann mit einem Mockobjekt konfigurieren und so den Test wesentlich lokaler und schneller gestalten. Das Problem mit Singletons ist nicht, dass sie existieren, sondern dass man drauf zugreift. Allein diesen Zugriff im gesamten Code zu vermeiden, stellt eine erhebliche Verbesserung der Testbarkeit und Wiederverwendbarkeit dar.

Extension Points

Der typische Weg, eigenen Code auf der Eclipse-Plattform auszuführen, ist einen der vielen Extension Points zu benutzen. Die allermeisten Extension-Points definieren neben dem XML-Schema auch ein oder mehrere Java-Interfaces. Wenn ich mich an diesem Punkt einhängen will, muss ich in meiner plugin.xml den Namen meiner Klasse angeben, die dieses Interface implementiert. Eclipse bzw. Equinox wird dann die Klasse instanziieren und an entsprechender Stelle benutzen. Moment, Equinox – instanziiert? Das geht ja nicht, weil die Klasse vom Guice Injector instanziiert werden muss, damit dieser auch die Abhängigkeiten in dieses Objekt injizieren kann. Das ist zum Glück kein Problem, weil Equinox hier das Konzept der so genannten IExecutableExtensionFactory anbietet. Wenn die in der plugin.xml angegebene Klasse nämlich dieses Interface implementiert, ruft Equinox daran die Methode create() auf und verwendet das davon zurückgegebene Objekt als Implementierung des Extension Points. Equinox überlässt mir damit die Möglichkeit, das Objekt selbst zu erzeugen. Damit die Factory weiß, welches Objekt erzeugt werden soll, kann im Extension Point nach einem Doppelpunkt noch zusätzliche Information hinterlegt werden. Dies stellt mir Equinox zur Verfügung, sobald mein Factory zusätzlich das Interface IExecutableExtension implementiert. Möchten Sie z. B. einen Action Handler anmelden, könnte die Konfiguration für die plugin.xml so aussehen:

Die referenzierte Klasse MyExecutableExtensionFactory kann generisch sein und für alle Extension Points wiederverwendet werden. In Listing 11 sehen Sie eine etwas vereinfachte Variante, der Sie evtl. noch ein bisschen mehr Fehlerbehandlung spendieren sollten:

Listing 11
public class MyExecutableExtensionFactory implements IExecutableExtensionFactory, 
                                                     IExecutableExtension {
  private String clazzName;

  public void setInitializationData(IConfigurationElement config,
                 String propertyName, Object data) throws CoreException {
    if (data instanceof String) {
      clazzName = (String) data;
    }
  }

  public Object create() throws CoreException {
    Class> class1 = Activator.getDefault().getBundle().loadClass(clazzName);
    return Activator.getDefault().getInjector().getInstance(class1);
  }

}

Wichtig ist, dass hier das eigene Bundle verwendet wird, um die Klasse zu laden. Weiterhin sehen Sie, dass wir hier auf den Injector verweisen. Richtig, das geschieht hier mit einem Singleton-Zugriff. Das ist aber auch die einzige Stelle, an der Sie in Zukunft den Injector referenzieren. Jegliche Objekte, die z. B. vom konfigurierten Action Handler (org.myproject.MyHandler) benötigt werden, werden automatisch von Guice injiziert.

Pro Bundle ein Injector

Der Code im letzten Abschnitt hat es schon suggeriert: Es hat sich bewährt, einen Injector pro Bundle zu erzeugen und diesen dann an den Lebenszyklus des Bundles zu binden. Das bedeutet, er wird erzeugt, wenn das Bundle gestartet wird und entfernt, wenn es gestoppt wird. Falls ein Bundle selbst keine Extension Points benutzt und auch keinen Zustand für andere Bundles zur Verfügung stellt, wird natürlich auch kein Injector benötigt.

public void start(BundleContext context) throws Exception {
  super.start(context);
  plugin = this;
  injector = Guice.createInjector(new MyBundleModule(context));
}
Weitere Unterstützung für OSGi

Das Stoppen eines Bundles möchten Sie natürlich eventuell auch auf mit Guice erzeugte und verwaltete Objekte propagieren. Für solche OSGi-spezifischen Funktionaliäten gibt es das Projekt Sisu [2] (bzw. Peaberry [3]). Hier werden nicht nur der Lifecycle von Bundles unterstützt, sondern vor allem auch die Injizierung von OSGi-Services. Das Tracking wird automatisch für Sie erledigt. Auf diese Themen einzugehen würde leider den Rahmen des Artikels sprengen.

Dependency Injection – Ganz oder gar nicht!

Wie Sie sehen, ist das Grundprinzip hinter Dependency Injection eigentlich sehr einfach zu verstehen. Guice unterstützt diese einfache Idee mit relativ wenigen orthogonalen Konzepten und die Einbindung in Eclipse-Plug-ins funktioniert dank eines problemlosen Zusammenspiels zwischen Equinox und Guice sehr gut. Ich kann Ihnen nur ans Herz legen diese Art der Komponentisierung einmal auszuprobieren. Dabei ist zu beachten, dass Sie Guice nicht einfach irgendwo im Projekt mal ausprobieren können. Sie können es in einem Bundle ausprobieren aber dieses sollten Sie dann komplett mit Guice konfigurieren, weil Sie sonst häufig den Bedarf haben werden, programmatisch auf den Injector zuzugreifen. Ihr Code wird mit Guice nicht nur wesentlich einfacher testbar und besser wiederverwendbar. Dependency Injection mit Guice hat darüber hinaus den netten Effekt, dass Sie automatisch beginnen mehrere und kleine statt wenige, aber große Schnittstellen zu entwerfen. Sie müssen sich eben nicht mehr um die Erzeugung der Dienste kümmern. Guice ist für mich ein Standardwerkzeug in jedem Java-Projekt geworden. Es macht schlicht sehr viel Spaß, an einem gut getestet, modularen und wartbaren Softwaresystem zu arbeiten.

Sven Efftinge ist bei der itemis AG (http://www.itemis.de) beschäftigt und maßgeblich für die Weiterentwicklung von Methoden und Werkzeugen für die modellgetriebene Softwareentwicklung verantwortlich. Er ist Mitglied des Eclipse Modeling PMC und leitet die Entwicklung des TMF-Xtext-Frameworks. Er spricht regelmäßig auf Softwarekonferenzen und ist Koautor des Buchs „Modellgetriebene Softwareentwicklung“ (dpunkt).
Kommentare

Schreibe einen Kommentar

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