Naked Objects - Objekte pur

Nackt und pur

Arno Haase

Grafische Nutzerschnittstellen von Geschäftsanwendungen werden oft als Abfolge von Masken entworfen. Dieser Trend wird von Web-Frameworks wie Struts unterstützt. Solche ablauforientierten Oberflächen passen natürlich gut zu den technologischen Beschränkungen von Browsern. Vor allem aber geben sie einen klaren Ablauf vor und präsentieren zu jedem Zeitpunkt nur eine kleine Auswahl an Funktionen. Dadurch wird insbesondere für unerfahrene Anwender die Arbeit mit dem System übersichtlicher.

Die Stärken dieses Ansatzes kommen recht gut bei Oberflächen zur Geltung, die für den Massenmarkt gedacht sind. Amazon, eBay oder auch die Fahrkartenautomaten der Deutschen Bahn sind Beispiele dafür, wie ein klar vorgegebener Ablauf Anwender an die Hand nimmt und Überforderung vermeidet. Bei firmenintern genutzten Geschäftsanwendungen fallen dieses Vorteile allerdings weit weniger ins Gewicht: Die typischen Anwender verwenden die Systeme so häufig, dass der Einarbeitungsaufwand weniger wichtig ist. Dafür beschränken ablauforientierte Systeme die Möglichkeiten des Anwenders darauf, genau die vorab festgelegten Abläufe in der vorgegebenen Reihenfolge auszuführen. Wenn sich in der Praxis Ausnahmen oder neue Abläufe ergeben – z.B. der Wunsch, Daten in einer anderen Reihenfolge zu erfassen, als die Maskenfolge vorgibt -, sind die Anwender gezwungen, das System auszutricksen oder an ihm vorbeizuarbeiten.

Objektorientierte Nutzerschnittstellen sind eine Alternative, die zu Unrecht ein Schattendasein führt. In ihnen werden die einzelnen Entitäten grafisch repräsentiert und der Anwender kann z.B. über ihr Kontextmenü mit ihnen interagieren. Vektor-Grafikprogramme oder Microsoft PowerPoint sind Beispiele für solche Nutzerschnittstellen. Solche Schnittstellen bieten dem Anwender im jeweiligen Kontext die ganze Funktionalität, die die Geschäftslogik gerade zur Verfügung stellt. Dadurch erlaubt eine solche Schnittstelle erfahreneren Anwendern, die ganze Bandbreite an möglicher Funktionalität ohne künstliche Einschränkungen zu nutzen.

Naked Objects

Eine konkrete Ausprägung solcher objektorientierter Nutzerschnittstellen ist der Naked Objects-Ansatz, der letztes Jahr in Buchform veröffentlicht wurde [1]. Es gibt auch ein Open Source-Framework, das zusammen mit ausführlicher Dokumentation im Internet bereitsteht [2]. Das Besondere an diesem Ansatz ist, dass die Geschäftsobjekte direkt und voll generisch an der Oberfläche repräsentiert werden. Jedes Objekt wird in einem eigenen Fenster dargestellt und im Kontextmenü dieses Fensters erscheinen die Methoden des Objektes.

Das Framework unterstützt auch generische Persistenz (per Default in eine RAM-Datenbank), sodass man ausschließlich die Geschäftsobjekte programmieren muss. Wenn man das Framework ein wenig kennt, kann man auf diese Weise eine erste Version eines Prototypen auch für ein nicht triviales System in einigen Stunden erstellen. Änderungen kann man z.B. während eines Analyseworkshops mehr oder weniger in Echtzeit vornehmen.

Beispiel: Videothek

Doch grau ist alle Theorie, deshalb zur Illustration: die Naked Objects-Implementierung einer vereinfachten Videothek-Software. Das System hat die Aufgabe, das Verleihen von DVDs an Kunden zu verwalten. Zwei Arten von Geschäftsobjekten, die dabei aller Wahrscheinlichkeit nach vorkommen, sind Kunde und Dvd, deshalb fangen wir einfach mit ihnen an. Die erste Version der Klasse Kunde ist in Listing 1 zu sehen.

Listing 1

public class Kunde extends AbstractNakedObject {
private final TextString _vorname = new TextString ();
private final TextString _nachname = new TextString ();
private final TextString _kundenNr = new TextString ();

public TextString getVorname () {return _vorname;}
public TextString getNachname () {return _nachname;}
public TextString getKundenNr () {return _kundenNr;}

public Title title () {
return new Title (_kundenNr).append (" (").append (_nachname).append ());
}
}

Damit sich das Framework um Präsentation und Persistenz kümmern kann, müssen die Klassen einige Anforderungen des Frameworks erfüllen. Zunächst müssen alle Geschäftsobjekte das Interface org.nakedobjects.object.NakedObject implementieren, das vor allem eine Reihe von Lifecycle-Methoden enthält. Das kann man am einfachsten erreichen, indem man sie von der abstrakten Basisklasse org.nakedobjects.object.AbstractNakedObject erben lässt, die Standardimplementierungen für diese Methoden bereitstellt.

Die einzige abstrakte Methode, die eine Unterklasse von AbstractNakedObject noch selbst implementieren muss, ist title. Zweck dieser Methode ist es, eine aus Sicht des Anwenders eindeutige String-Darstellung des Objektes zu liefern, die das Framework bei der Darstellung als Kurzform des Objektes verwenden kann. In unserem Beispiel verwenden wir für den Kunden seine Kundennummer mit dem Nachnamen in Klammern dahinter. Außerdem müssen alle Attribute, die dargestellt oder persistent gespeichert werden sollen, das Interface org.nakedobjects.object.value.NakedValue implementieren, das diverse Methoden zur Verwaltung und Organisation der Daten enthält. Für die Basis-Datentypen bringt das Framework schon entsprechende Klassen mit, z.B. org.nakedobjects.object.value.TextString oder org.nakedobjects.object.value.Date.

Anders als sonst in Java üblich sind bei Naked Objects die Klassen für die primitiven Attribute veränderbar. So hat z.B. die Klasse TextString Methoden zum Verändern ihres Inhalts, dafür braucht unsere Klasse Kunde nur eine get-Methode für das Attribut nachname. Das Framework analysiert zur Laufzeit per Reflection die Klassen und ermittelt die vorhandenen Attribute anhand des Vorhandenseins von get-Methoden ohne Parameter, deren Rückgabetyp eine Unterklasse von NakedValue ist.

Beziehungen zwischen Objekten

Kunden haben normalerweise Adressen. In diesem einfachen Beispiel ist es vielleicht etwas übertrieben, den Adressen eine eigene Klasse zu spendieren, aber in vielen realen Systemen ist das eine gute Entscheidung. Außerdem illustriert es, wie Beziehungen zwischen Geschäftsobjekten implementiert werden. Ein Kunde soll hier eine beliebige Anzahl an Adressen haben können.

Wir haben also eine dritte Klasse Adresse, die sich zunächst durch nichts von den ersten beiden unterscheidet. Die Klasse Kunde benötigt als weiteres Attribut eine Collection mit den Adressen sowie eine get– und eine add-Methode für Adressen (Listing 2).

Listing 2

public class Kunde extends AbstractNakedObject {
.
private final InternalCollection _adressen = new InternalCollection (Adresse.class, this);
public InternalCollection getAdressen () {return _adressen;}
.
}

Für Collections muss man – wie schon für die primitiven Attribute – eine Klasse aus dem Framework verwenden, hier die Klasse org.nakedobjects.object.collection.InternalCollection. Die get-Methode liefert in naheliegender Weise die gesamte Collection zurück und erlaubt damit dem Framework Zugriff auf die assoziierten Objekte. Das Anlegen einer neuen Adresse soll über das Kontext-Menü eines Kunden möglich sein. Dazu bekommt die Klasse Kunde eine action-Methode (Listing 3).

Listing 3

public class Kunde extends AbstractNakedObject {
.
public Adresse actionNeueAdresse () {
final Adresse result = (Adresse) createInstance (Adresse.class);
_adressen.add (result);
objectChanged ();
return result;
}
.
}

Das ist wieder ein Beispiel für die Verwendung von Reflection auf Basis von Namenskonventionen. Jede Methode, deren Name mit action beginnt, wird im Kontextmenü eingetragen und kann dadurch von Anwendern aufgerufen werden. Wenn die Methode keine Parameter hat und ihr Rückgabetyp ein Geschäftsobjekt ist, wird das zurückgegebene Geschäftsobjekt anschließend vom Framework in einem eigenen Fenster geöffnet.

Bei der Implementierung der action-Methode sind einige Besonderheiten zu beachten. So wird die neue Instanz von Adresse nicht direkt über ihren Konstruktor, sondern durch einen Aufruf der Methode createInstance von AbstractNakedObject erzeugt. Diese Methode kümmert sich um die Persistenz des neu erzeugten Objektes. Außerdem wird am Ende die Methode objectChanged aufgerufen, die das Framework benachrichtigt, dass Änderungen persistiert und auf dem Bildschirm aktualisiert werden müssen. Methoden zum Löschen oder Kopieren einer Adresse ließen sich ganz analog implementieren.

Exploration

Jetzt brauchen wir nur noch ein Stück Code, das das Ganze zusammenbindet und dem Framework als Einstiegspunkt dient. Eine solche Klasse heißt in der Naked Objects-Terminologie Exploration und ihre einzige Aufgabe besteht darin, dem Framework mitzuteilen, welche Klassen von Geschäftsobjekten es gibt. Die Klasse Adresse tragen wir nicht in die Liste der Klassen ein, weil Adressen nicht eigenständig als Top-Level-Klassen erscheinen sollen.

Listing 4

Die Exploration-Klasse

public class VideothekExploration extends Exploration {
public void classSet (NakedClassList classes) {
classes.addClass (Kunde.class);
classes.addClass (Dvd.class);
}

public static void main (String[] args) {
new VideothekExploration ();
}
}

Die Exploration erbt von der abstrakten Klasse org.nakedobjects.Exploration und implementiert die Methode classSet, in der sie alle Geschäftsobjektklassen beim Framework anmeldet (Listing 4). Außerdem erzeugt sie in einer main-Methode eine Instanz und der Konstruktor der Basisklasse Exploration erzeugt die Fenster und startet die Anwendung; die Anwendung selbst läuft dann im Swing-Thread.

Eine letzte Voraussetzung müssen wir noch schaffen, bevor wir die Anwendung starten können: Für jede Klasse von Geschäftsobjekten muss ein Icon in 32-x-32 Pixel-Auflösung vorliegen. Die Grafikdateien müssen im Verzeichnis images liegen und den Namen der jeweiligen Klasse haben, für die Klasse Kunde also images/Kunde.gif. Diese Icons verwenden das Framework, um die Klassen grafisch zu repräsentieren und Geschäftsobjekte verschiedenen Typs optisch auf den ersten Blick voneinander abzuheben.

Nach dem Starten der Anwendung erscheint ein Fenster, das für jede Klasse ihr Icon enthält (Abb. 1). Im Kontextmenü des jeweiligen Icons erscheinen Methoden zum Erzeugen und Finden von Instanzen der jeweiligen Klasse, die vom Framework bereitgestellt werden.

Abb. 1: Top-Level-Klassen mit Kontextmenü

Da es bisher keine Kunden gibt, legen wir einen neuen Kunden an. Das Framework öffnet ein neues Fenster, das den neuen Kunden repräsentiert und dessen Attribute anzeigt. Man kann jetzt die Felder ausfüllen und ändern und jede Änderung wird beim Verlassen des Feldes persistiert. Oben im Fenster sind das Kunde-Icon und die eindeutige String-Darstellung dieses Kunden, die mit der title-Methode erzeugt wird. Diese beiden Merkmale helfen dabei, den Überblick zu behalten, wenn mehrere Fenster mit verschiedenen Objekten offen sind (Abb. 2).

Abb. 2: Grafische Repräsentation von Geschäftsobjekten

Zunächst ist die Liste der Adressen des Kunden leer und statt der Liste der Adressen erscheint nur ein Platzhalter. Um das zu ändern, wählen wir Neue Adresse im Kontextmenü des Kunden – der Name des Menüeintrags wird vom Framework direkt aus dem Namen der entsprechenden Methode abgeleitet. Das dadurch erzeugte Adress-Objekt wird in einem eigenen Fenster angezeigt. Außerdem wird es direkt im Kunden-Fenster angezeigt und eine Änderung an der Adresse wirkt sich unmittelbar auf beide Fenster aus.

Constraints und Abläufe

Damit haben wir die Framework-Elemente, um ein vollständiges Videoausleihsystem zu bauen, in dem allerdings jeder Anwender jederzeit alle Attribute und Objektbeziehungen ändern kann. Um das einzuschränken, kann man dem Framework Metainformationen zu Attributen und Methoden geben, die z.B. festlegen, ob ein Attribut gerade änderbar oder eine Methode aufrufbar ist. Als einfaches Beispiel nehmen wir an, die Kundennummer wird nicht mehr manuell erfasst, sondern bei der Erzeugung vom System vergeben und soll später nicht mehr änderbar sein (Listing 5).

Listing 5

public class Kunde extends AbstractNakedObject {
private static int _nextKundenNr = 1;
.
public void created () {
_kundenNr.setValue ( + (_nextKundenNr++));
}
public About aboutKundenNr () {
return FieldAbout.READ_ONLY;
}
.
}

Die Methode created() ist ein Hook, der vom Framework für jede logische Instanz genau einmal aufgerufen wird. Die Methode aboutKundenNr wird vom Framework per Namenskonvention erkannt, weil sie mit about beginnt und About als Rückgabetyp hat. Weil der Methodenname auf KundenNr endet, beziehen sich die zurückgelieferten Metadaten auf die get-Methode getKundenNr.

Damit haben wir alles beisammen, um auch das eigentliche Ausleihen von DVDs zu implementieren. Die Ausleihvorgänge werden im Naked Objects-Ansatz wie alles andere durch Geschäftsobjekte dargestellt. Diese Geschäftsobjekte unterscheiden sich von den bisherigen Objekten aber dadurch, dass sie zielgerichtet sind: Eine Ausleihe kann in unserem einfachen Beispiel zwei Zustände haben, ausgeliehen und zurückgegeben, aber eine zurückgegebene Ausleihe kann nicht wieder auf ausgeliehen zurückgesetzt werden. Im allgemeinen Fall haben solche Geschäftsobjekte eine State Machine, die die möglichen Übergänge definiert.

Die Listings auf der Heft-CD zeigen die vollständige Anwendung. Die Klasse Ausleihe hat ein Ausleih- und ein Rückgabedatum und eine Referenz auf den ausleihenden Kunden sowie die ausgeliehene DVD. Die Beziehung zur DVD ist bidirektional: Eine DVD hat eine Referenz auf den Ausleihvorgang, sofern sie gerade ausgeliehen ist.
Ein Ausleiheobjekt wird dadurch erzeugt, dass man per Drag & Drop einen Kunden auf ein DVD-Objekt zieht. Das wird in Form der actionAusleihen-Methode implementiert. Da der Methodenname mit action beginnt, handelt es sich um eine Operation, die der Anwender initiieren kann; da die Methode ein Geschäftsobjekt als Parameter bekommt, wird sie durch Drag & Drop initiiert; und da die Methode ein Geschäftsobjekt zurückliefert, wird dieses Geschäftsobjekt anschließend in einem neuen Fenster dargestellt. Die aboutActionAusleihen-Methode stellt sicher, dass man eine DVD nur dann ausleihen kann, wenn sie aktuell nicht ausgeliehen ist.

Die Methode actionDvdZurück der Klasse Ausleihe ändert den internen Zustand der Ausleihe und markiert die DVD als nicht mehr ausgeliehen. Aufgrund des geänderten Zustandes wird jetzt die Methode actionDvdZurück gesperrt, sodass man sie nur einmal aufrufen kann.

Stärken und Beschränkungen

Dieses Beispiel zeigt, wie ein System aussieht, das mit dem Naked Objects-Framework entwickelt wird. Auf der Grundlage dieses kleinen Systems lassen sich die zentralen Stärken von Naked Objects nachvollziehen. Diese Art von Nutzerschnittstelle gibt dem Anwender alle Möglichkeiten, die durch die fachlichen Constraints nicht ausgeschlossen sind. Das geht oft erheblich über die Möglichkeiten von Systemen hinaus, die streng ablauforientiert eine Abfolge von Masken bieten. Das interne Design der Anwendung wird für Anwender sichtbar. Damit entsteht eine gemeinsame Sprache für Entwickler und Fachabteilung, um über die Anforderungen zu reden.

Das Framework erlaubt es, in sehr kurzer Zeit einen halbwegs funktionalen Prototypen zu erstellen und auf diesem Weg das Verständnis für das Problem zu vertiefen sowie Feedback zu Annahmen zu erhalten. Diese Art von Systemen ist sehr gut testbar, weil man anstelle des GUI-Frameworks einen Test-Client verwenden kann, der die gleichen Methoden aufruft, wie sie sonst durch Nutzerinteraktion aufgerufen würden. Das Naked Objects-Framework bietet dazu sehr komfortable Unterstützung. Naked Objects bieten die Möglichkeit, die Anwendung statt in Fenstern als Browser-Anwendung zu betreiben. Aufgrund der voll generischen Nutzerschnittstelle erfordert diese Umstellung keine Änderungen am Programm-Code.

Andererseits hat Naked Objects zumindest auf seinem aktuellen Stand auch einige Beschränkungen. Zurzeit hat man keine Kontrolle über Transaktionsgrenzen, denn das Framework persistiert bei jedem Verlassen eines Feldes das betroffene Objekt, was zu erheblichen Skalierungsproblemen führen dürfte. Außerdem kann eine Anwendung damit nicht sicherstellen, dass der Zustand der Datenbank gewissen Konsistenzbedingungen genügt, womit allerdings zumindest ein Naked Objects GUI keine Probleme hat.

Alle Eigenschaften und Funktionen liegen direkt in den jeweiligen Geschäftsobjekten, was bei größeren Anwendungen zu einer schlechten Trennung der Concerns führen kann. Zum Beispiel werden Aspekte wie die Änderungshistorie eines Geschäftsobjektes sowohl an der Oberfläche als auch intern meist völlig separat behandelt – der Werkzeug-Material-Ansatz bietet hier mehr Flexibilität [3].

Das Framework ist an einigen Stellen noch etwas hakelig, z.B. bestehen Repaint-Probleme. Diese Beschränkungen beziehen sich auf die aktuelle Version 1.2.1 des Frameworks. Angesichts der Stärken kann man auf die weitere Entwicklung gespannt sein.

Fazit

Objektorientierte Nutzerschnittstellen sind für Sachbearbeitersysteme eine bewährte Alternative zu einer rein ablauforientierten Maskenfolge, weil sie die Anwender weniger in dem einschränken, was sie tun können, ohne dass dadurch die Integrität der Geschäftslogik gefährdet wäre. Der Naked Objects-Ansatz ist eine besonders konsequente Umsetzung dieses Ansatzes und der griffige Name wird sicher zu seiner Verbreitung beitragen. Das Framework ist gut für Analyse-Workshops mit Rapid Prototyping geeignet und es lohnt sich, es kennen zu lernen und seine weitere Entwicklung zu verfolgen.

Arno Haase (Arno.Haase@Haase-Consulting.com) ist freiberuflich tätiger Softwarearchitekt. Er interessiert sich besonders für verteilte Systeme und modellbasierte Systementwicklung.

Links und Literatur

Geschrieben von
Arno Haase
Kommentare

Schreibe einen Kommentar

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