Donnerstag, 10. Juni 2010

Extension-Points, Extensions, Adapter und AdapterFactories

Eclipse kann man sich als ein Puzzle vorstellen, deren einzelne Funktionen auf die Puzzle-Teile verteilt sind und sich gegenseitig zu einem Gesamtkonzept verbinden. Dabei entspricht einem Puzzle-Teil eine abgegrenzte Menge von Funktionalität, die über einen eigenen Klassenpfad und Sichtbarkeitsbereich verfügt. Um aber als Puzzle-Teil gebrauch von Funktionalität seitens anderer Puzzle-Teile (Plug-ins) machen zu können und seinerseits anderen Plug-ins die eigene Funktionalität zur Verfügung zu stellen, gibt es sogenannte Extension-Points.


Extension-Points

Einem Extension-Point eines Plug-ins entspricht bildlich eine Steckdosenleiste mit festgelegter spezifischer Buchsenform; so zum Beispiel Flachstecker-Buchse oder Rund-Buchse. Dieser bildlichen Umschreibung eines Extension-Points entspricht softwaretechnisch ein Interface für den jeweiligen Extension-Point. Ein Plug-in stellt über Extension-Points eine API bereit, mit der es Benutzern der Extension-Points mitteilt, welche Funktionserweiterungen eingebunden werden können. So kann ein Plug-in z.B. über einen Extension-Point anderen Plug-ins ermöglichen sich als Event-Listener zu registrieren oder aber Menüeinträge und Symbolleisten zu erweitern, indem Attribute wie Beschriftung, Symboldatei usw. als zu setzende Parameter definiert und evtl. ein Interface von Seiten des Extension-Point-Anbieters vorgegeben wird.
Die Parameter des Extension-Points definiert man in einer sogenannten *.exsd Datei, welche als Atrribut vom
<extension-point>
Element in der Datei plugin.xml verlinkt wird. Diese *.exsd Datei wird ausgelesen, wenn man sich die Beschreibung eines Extension-Points im "Plug-in Manifest Editor" der Eclipse PDE Perspektive anzeigen läßt.





Extensions

Jede Benutzung eines Extension-Points stellt eine Extension dar. In der plugin.xml kommt dies durch das xml Element extension zum Ausdruck. Im Unterschied zur Definition eines Extension-Points wird bei der Erstellung einer Extension
<extension point="...">
statt
<extension-point id="...">
geschrieben. Dabei enthält das Attribut point den Wert des Attributes id der Extension-Point Definition, allerdings in vollqualifizierter Schreibweise mit ID des den Extension-point definerenden Plug-ins.

Eine Extension wird also vom Extension-Point definerenden Plug-in fremdaufgerufen. Dies entspricht dem Hollywood Prinzip: Don't call us, we call you!


Adapter

Die zweite Möglichkeit eigenen Code in das bestehende Plug-in-Geflecht von Eclipse einzuschleusen ist in der Definition von Adaptern gegeben. Ein Adapter bietet die Möglichkeit eine bestehende Klasse im Nachhinein um Interface Implementierungen zu erweitern. Dabei wird die Signatur der Klasse also nicht verändert und es treten keine Konflikte mit davon abhängenden anderen Klassen auf. Was passiert ist vielmehr ein Eingriff zur Laufzeit. Nahezu jede öffentliche Klasse eines Plug-ins implementiert hierzu das Interface IAdaptable. Wenn man nun später weitere Interfaces unterstützen möchte ohne die Signatur der Klasse ändern zu müssen, überschreibt man einfach die einzige Methode aus IAdaptable, getAdapter(Class c).
Darin schreibt man dann sowas wie:
class B extends A
{
    private NewInterface delegate = new NewInterfaceImpl();
    class NewInterfaceImpl implements NewInterface
    {
        //Innere Klasse hat vollen Zugriff auf A mittels A.this
        public f1()
        {
            //benutze Methoden von A so, dass f1 sinnvoll realisiert wird.
        }
        ...
    }
    public Object getAdapter(Class c)
    {
        if (c == NewInterface.class)
        return delegate;
        else
        super.getAdapter(c);
    }
}
Falls nun jemand Gebrauch von dem neuen Interface machen will, so kann er ganz leicht überprüfen, ob dieses Interface unterstützt wird, indem er folgendes schreibt:
A a = ...;
Object ni = null;
if((ni=a.getAdapter(NewInterface.class))!=null) ((NewInterface) ni).f1();

Auf diese Weise kann man also die dynamische Signatur einer Klasse ändern.
Was aber hat das mit dem einbringen von eigenem Code in das bestehende Plug-in-Geflecht zu tun?
Wir können ja nicht davon ausgehen, dass wir die jeweilige Klasse überschreiben und dann statt der Originalklasse unsere instantiiert wird. Daher gibt es noch eine zweite Möglichkeit der Nutzung des Adapter Prinzips


AdapterFactories

Eine AdapterFactory ist eine Klasse, welche das Interface IAdapterFactory implementiert. Dieses Interface hat nur die beiden Methoden
//Gibt Liste aller unterstützten Adapter zurück
public Class[] getAdapterList();
/*adaptableObject teilt der Fabrik mit, welches Objekt um einen entsprechenden Adapter vom Typ adapterType gefragt wurde.
*/
public Object getAdapter(Object adaptableObject, Class adapterType);

Eine Fabrik verwaltet also eine Menge von Adaptern im Auftrag eines anderen Objektes. Die Verknüfung von Fabrik und Klient, welcher wie nachfolgend begründet vom Typ IAdaptable ist, erfolgt über einen systemweiten Adaptermanager. Dieser Adaptermanager implementiert das Interface IAdapterManager mit u.a. folgender Methode
public void registerAdapters(IAdapterFactory factory, Class client);
Auf den systemweiten Adaptermanager erhält man Zugriff über 
Platform.getAdapterManager()
und kann nun (den Objekten) einer Klasse eine entsprechende Fabrik zuordnen. Dies geschieht auf Grundlage der Nennung des entsprechenden Grundtyps, auf den hin die Fabrik aktiv werden soll.

Was jetzt geschieht ist folgendes:
Ein Objekt, welches eine Fabrik damit beauftragen will, entsprechende Adapter für ihn zu produzieren, implementiert die Methode aus IAdaptable auf folgende weise:
public Object getAdapter(Class adapterType){
    return Platform.getAdapterManager().getAdapter(this,adapterType);
}

Der Adaptermanager durchläuft nun die Vererbungshierarchie des Aufrufers von unten nach oben bis zur Klasse Object und fragt in jeder Stufe mit dem enstprechenden Klassenobjekt (Typ Class) als Schlüssel die Hashtabelle nach registrierten Fabriken. Ob der gesuchte Adapter von der Fabrik unterstützt wird erkennt der Adaptermanager durch Aufruf der Fabrikfunktion getAdapterList(). Im Erfolgsfall wird die Suche frühzeitig beendet und die entsprechende Fabrik folgendermaßen aufgerufen.
//innerhalb von getAdapter(Object adaptableObject,Class adapterType) vom IAdapterManager
...
//Nachfolgend Pseudo-Code
IAdapterFactory fab;
fab = findFactory(adaptableObject,adapterType);
return fab.getAdapter(adaptableObject,adapterType);
...
Der Grundgedanke dazu ist folgender:
Eine Klasse hat ja eine Vererbungshierarchie, welche aus Klassen und Interfaces bestehen kann. Gehen wir von folgender Vererbungshierarchie aus:
public class C extends B implements IC_1, IC_2 {...}

public class B extends A implements IF {...}

public class A {...}


Wenn wir nun ein Objekt
Object obj = new C();
erzeugen, dann ist folgendes wahr:
(obj instanceof C) && (obj instanceof B) && (obj instanceof A) &&
(obj instanceof Object) && (obj instanceof IC_1) && (obj instanceof IC_2) && (obj instanceof IF)
== true

Wir haben also für die Registrierung der Adapterfabrik für die Klasse C folgende Möglichkeiten:
//Konkrete Fabrik muss je nach gewählter Alternative implementiert werden. 
IAdapterFactory fab = ...;

IAdapterManager man = Platform.getAdapterManager();

1. man.registerAdapters(fab,C.class);
2. man.registerAdapters(fab,B.class);
3. man.registerAdapters(fab,A.class);
4. man.registerAdapters(fab,Object.class);
5. man.registerAdapters(fab,IC_1.class);
6. man.registerAdapters(fab,IC_2.class);
7. man.registerAdapters(fab,IF.class);


Bei Variante 1 sieht die Fabrik so aus:
class C_casting_Factory implements IAdapterFactory
{
    ...
    Object getAdapter(Object client,
    Class adapterType){
        C casted = (C) client;
        if(adapterType==Adapter1.class){
            //Konstruktor: Adapter1_impl(C client);
            return new Adapter1_impl(casted);
        }
        elseif(adapterType==Adapter2.class){
            //Konstruktor: Adapter2_impl(C client);
            return new Adapter2_impl(casted);
        }
        ...
    }
}

und bei Variante 3 so:
class A_casting_Factory implements IAdapterFactory
{
    ...
    Object getAdapter(Object client,
    Class adapterType){
        A casted = (A) client;
        if(adapterType==Adapter1.class){
            //Konstruktor: Adapter1_impl(A client);
            return new Adapter1_impl(casted);
        }
        elseif(adapterType==Adapter2.class){
            //Konstruktor: Adapter2_impl(A client);
            return new Adapter2_impl(casted);
        }
        ...
    }
}

Dadurch kann es vorkommen, das für eine Klasse gleich mehrere Fabriken in Frage kommen, welche den angefragten Adapter zurückgeben können. In diesem Fall gilt die Regel. Die Fabrik, welche mit dem spezifischsten Klassenobjekt regsitriert wurde erhält den Zuschlag und darf den entsprechenden Adapter zurückgeben.


Kommen wir nun zurück zu unserem Ausgangsgedanken:
Wir wollen von externer Seite neue Adapter für eine Klasse hinzufügen oder bestehende Adapter überschreiben.
Dazu kann man den Extension-Point
org.eclipse.core.runtime.adapters
verwenden oder aber zur Laufzeit folgenden Code
Platform.getAdapterManager().registerAdapters(fab,client)
schreiben.

Eine mögliche Extension könnte folgendermaßen aussehen:
<extension point="org.eclipse.core.runtime.adapters">
  <factory adaptabletype="pfad.zum.klienten.der.fabrik"
      class="pfad.zur.fabrik.klasse">
    <adapter type="pfad.klasse.adapter1"/>
    <adapter type="pfad.klasse.adapter2"/>
  </factory>
</extension>

Dabei ist die Aufzählung der Adapter nur als deklarative Wiederholung von der Fabrik-Methode
getAdapterList()
zu verstehen. Die obige Extension Variante der Fabrikregistrierung ersetzt die Notwendigkeit bei Plug-in Aktivierung folgenden Code schreiben zu müssen:
Platform.getAdapterManager().registerAdapters(pfad.zur.fabrik.klasse,pfad.zum.klienten.der.fabrik);

Die registrierte Fabrik wird aber nur dann bei der Adaptersuche miteinbezogen, wenn das die Fabrik definierende Plug-in geladen ist. Das bloße definieren der Extension hat nicht die Aktivierung des zugehörigen Plug-ins zur Folge.


Falls noch irgend etwas unklar sein sollte, dann schreibt mir einen Kommentar dazu.