next up previous
Nächste Seite: Operationen ohne zentrale Auftragnehmer Aufwärts: OO-Techniken in Oberon Vorherige Seite: Generische Module


Aufträge und Auftragnehmer

Die Technik des late binding wird auf Basis von Record-Komponenten erreicht, die einen Prozedurtyp besitzen. Im einfachsten Fall gibt es genau eine solche Komponente, die alle Operationen von außerhalb entgegennimmt:

TYPE
   Message = RECORD END; (* Basistyp fuer alle Auftraege *)
   Object = POINTER TO ObjectRec;
   Handler = (* Auftragnehmer *)
      PROCEDURE (object: Object; VAR message: Message);
   ObjectRec =
      RECORD
         handler: Handler;
      END;

Um die unterschiedlichen Operationen mit ihren Parametern zu repräsentieren, können entsprechende Erweiterungen von Message definiert werden. Da ein Auftragnehmer den Auftrag als Variablenparameter erhält, ist es möglich, auch Werte wieder zurückzugeben.

Im Beispiel der Figuren könnte dies folgendermaßen aussehen:

TYPE
   MoveMessage =
      RECORD
         (Message)
         newx, newy: INTEGER;
      END;
   DrawMessage =
      RECORD
         (Message)
      END;
   Figure = POINTER TO FigureRec;
   FigureRec =
      RECORD
         (ObjectRec)
         x, y: INTEGER;
      END;

Ein Auftragnehmer kann dann aufgrund von Typentests die einzelnen Operationen voneinander unterscheiden. Folgendes Beispiel deutet einen Auftragnehmer für Kreise an:

PROCEDURE Handler(figure: Figure; VAR message: Message);
BEGIN
   WITH figure: Circle DO
      IF message IS MoveMessage THEN
         WITH message: MoveMessage DO
            (* Kreis bewegen *)
         END;
      ELSIF message IS DrawMessage THEN
         WITH message: DrawMessage DO
            (* Kreis zeichnen *)
         END;
      ELSE
         (* unbekannter Auftrag *)
      END;
   END;
END Handler;

Da der Prozedurtyp für Auftragnehmer nur den Basistyp Figure festlegt, wird eine regionale Typzusicherung benötigt, um auf die Komponenten der Erweiterung zugreifen zu können. Andererseits könnte ein Aufruf einer Move-Operation folgendermaßen aussehen:

PROCEDURE MoveFigure(figure: Figure; newx, newy: INTEGER);
   VAR
      moveMessage: MoveMessage;
BEGIN
   moveMessage.newx := newx; moveMessage.newy := newy;
   figure.handler(figure, moveMessage);
END MoveFigure;

Zwei Nachteile der vorgestellten Lösung ergeben sich aus dem ungehinderten Zugang zu der Komponente für den Auftragnehmer und der Umständlichkeit, einen Auftrag abzuschicken. Diese Probleme lassen sich jedoch gleich bei dem Modul, das die Aufträge und den Typ für den Auftragnehmer definiert, erledigen.

Eine weitere offene Frage ist natürlich, ob Aufträge auch bearbeitet worden sind bzw. was Auftragnehmer bei unbekannten Aufträgen tun sollen. Ein Erledigungsvermerk als zusätzliche Komponente bei den Aufträgen wäre eine mögliche Lösung. Dann können Auftragnehmer unbekannte Aufträge ignorieren, ohne daß dies unbemerkt bleibt.

Folgendes Beispiel zeigt eine vereinfachte Abstraktion für unstrukturierte Sammlungen von Objekten, die sowohl eine konventionelle Schnittstelle für Klienten anbietet als auch eine auf Auftragnehmer basierende Schnittstelle für Implementierungen. Auf Erledigungsvermerke wird in diesem Beispiel verzichtet.

DEFINITION Collections;

   IMPORT Disciplines, Objects;

   TYPE
      Collection = POINTER TO CollectionRec;
      CollectionRec = RECORD (Disciplines.ObjectRec) END;

   (* Schnittstelle fuer Implementierungen *)

   TYPE
      Message = (* Basistyp fuer alle Auftraege an Collections *)
         RECORD END;
      Handler = (* Auftragnehmer *)
         PROCEDURE (collection: Collection; VAR message: Message);

      AddMessage =
         RECORD
            (Message)
            object: Objects.Object;
         END;
      FirstMessage = RECORD (Message) END;
      NextMessage =
         RECORD
            (Message)
            (* Rueckgabewerte *)
            object: Objects.Object;
            endOfCollection: BOOLEAN;
         END;

   PROCEDURE SetHandler(collection: Collection; handler: Handler);

   (* Schnittstelle fuer Klienten *)

   PROCEDURE Add(collection: Collection; object: Objects.Object);

   PROCEDURE First(collection: Collection);
   PROCEDURE Next(collection: Collection;
                  VAR object: Objects.Object) : BOOLEAN;

END Collections.

Wie man an diesem Beispiel sieht, gibt es bei Aufträgen sowohl Parameter, die übergeben werden, als auch Werte, die man zurückerhält. Bei Next wird der RETURN-Wert explizit durch die Komponente endOfCollection bestimmt.

Die Komponente handler gehört nicht dem öffentlichen Teil an und ist nur über SetHandler veränderbar, die von der jeweiligen Implementierung zu Beginn aufgerufen wird. Das Modul Collections ist selbst keine Implementierung mehr in dem Sinne, daß es eine Sammlung von Objekten verwaltet, sondern es dient nur noch dazu, die Operationen in Aufträge zu verwandeln und an die eigentliche Implementierung zu verschicken:

MODULE Collections;

   IMPORT Disciplines, Objects;

   TYPE
      Message = (* Basistyp fuer alle Auftraege an Collections *)
         RECORD END;
      Collection = POINTER TO CollectionRec;
      Handler = (* Auftragnehmer *)
         PROCEDURE (collection: Collection; VAR message: Message);
      CollectionRec =
         RECORD
            (Disciplines.ObjectRec)
            (* private Komponenten *)
            handler: Handler;
         END;

   (* Schnittstelle fuer Implementierungen *)

   TYPE
      AddMessage =
         RECORD
            (Message)
            object: Objects.Object;
         END;
      FirstMessage = RECORD (Message) END;
      NextMessage =
         RECORD
            (Message)
            (* Rueckgabewerte *)
            object: Objects.Object;
            endOfCollection: BOOLEAN;
         END;

   PROCEDURE SetHandler(collection: Collection; handler: Handler);
   BEGIN
      collection.handler := handler;
   END SetHandler;

   (* Schnittstelle fuer Klienten *)

   PROCEDURE Add(collection: Collection; object: Objects.Object);
      VAR
         message: AddMessage;
   BEGIN
      message.object := object;
      collection.handler(collection, message);
   END Add;

   PROCEDURE First(collection: Collection);
      VAR
         message: FirstMessage;
   BEGIN
      collection.handler(collection, message);
   END First;

   PROCEDURE Next(collection: Collection;
                  VAR object: Objects.Object) : BOOLEAN;
      VAR
         message: NextMessage;
   BEGIN
      collection.handler(collection, message);
      IF message.endOfCollection THEN
         RETURN FALSE
      ELSE
         object := message.object;
         RETURN TRUE
      END;
   END Next;

END Collections.

Das bereits vorgestellte Modul für lineare Listen wäre ein geeigneter Kandidat für eine mögliche Implementierung von Collections. Dabei läßt sich die Schnittstelle von LinearLists radikal verkürzen, da hier nur noch Create benötigt wird. Alle anderen Operationen erfolgen dann über Collections:

DEFINITION LinearLists;

   IMPORT Collections;

   PROCEDURE Create(VAR collection: Collections.Collection);

END LinearLists.

Alternativ wäre es auch denkbar, den Typ der Liste zu offenbaren. Dies würde entsprechende Typentests erlauben:

DEFINITION LinearLists;

   IMPORT Collections;

   TYPE
      List = POINTER TO ListRec;
      ListRec = RECORD (Collections.CollectionRec) END;

   PROCEDURE Create(VAR collection: Collections.Collection);

END LinearLists.

Wichtig ist, daß selbst bei Offenlegung des abgeleiteten Typs Create den Typ der zugehörigen Abstraktion zurückliefert. Andernfalls wäre ein Anwender gezwungen, die zugehörige Variable oder Komponente mit dem Typ LinearLists.List zu deklarieren. Dies würde in folgendem Beispiel die Einführung von Hilfsvariablen erzwingen:

VAR
   collection: Collections.Collection;

(* ... *)

IF (* eine Liste ist vorzuziehen *) THEN
   LinearLists.Create(collection);
ELSIF (* ein binaerer Baum ist guenstiger *) THEN
   BinTrees.Create(collection);
(* ... *)
END;

Bei der Implementierung von LinearLists kommt im wesentlichen nur Handler hinzu, der die bereits bekannten Operationen verwendet, die jetzt privat geworden sind. Create hat als lokale Variable eine Liste vom erweiterten Typ, damit NEW die gewünschte Erweiterung anlegt. Ansonsten darf bei Create nicht die Verknüpfung des neu kreierten Objekts mit dem zugehörigen Auftragnehmer vergessen werden.

MODULE LinearLists;

   IMPORT Collections, Objects;

   TYPE
      Linkable = POINTER TO LinkableRec;
      LinkableRec =
         RECORD
            next: Linkable;
            object: Objects.Object;
         END;

      List = POINTER TO ListRec;
      ListRec =
         RECORD
            (Collections.CollectionRec)
            (* private Komponenten *)
            head, tail: Linkable;
            cursor: Linkable; (* fuer List und Next *)
         END;

   (* Append, First und Next bleiben unveraendert *)

   PROCEDURE Handler(collection: Collections.Collection;
                     VAR message: Collections.Message);
   BEGIN
      WITH collection: List DO
         IF message IS AddMessage THEN
            WITH message: AddMessage DO
               Append(collection, message.object);
            END;
         ELSIF message IS FirstMessage THEN
            First(collection);
         ELSIF message IS NextMessage THEN
            WITH message: NextMessage DO
               message.endOfCollection := ~Next(collection, message.object);
            END;
         END;
      END;
   END Handler;

   PROCEDURE Create(VAR collection: Collections.Collection);
      VAR
         list: List;
   BEGIN
      NEW(list);
      list.head := NIL; list.tail := NIL; list.cursor := NIL;
      Collections.SetHandler(list, Handler);
      collection := list;
   END Create;

END LinearLists.

Bemerkenswert ist bei dieser Technik, daß der Kontrollfluß anders als in klassischen objekt-orientierten Sprachen verläuft. Abbildung 2.2

Abbildung: Unterschiedlicher Kontrollfluß bei Oberon und klassischen OO-Sprachen
\begin{figure}\epsfig{file=method-invocation.eps}\end{figure}

zeigt, daß bei Oberon zunächst die Operation bei dem Modul aufgerufen wird, das die Abstraktion definiert, und erst danach (möglicherweise sogar nur optional) die zu dem Objekt gehörende Operation durchgeführt wird - eine Technik, die der von BETA ähnelt. Das steht ganz im Gegensatz zu den klassischen objekt-orientierten Sprachen, bei denen zunächst die zum Objekt gehörende Operation aufgerufen wird, die dann optional die überdefinierte Operation der darüberliegenden Klasse aufrufen kann.

Beide Varianten haben ihre Vor- und Nachteile. Sicherlich ist die traditionelle Variante schneller, wenn die Operation der übergeordneten Klasse nicht aufgerufen wird. Andererseits ermöglicht Oberon unterschiedliche Schnittstellen für die Klienten und die Implementierungen. Wenn man Schnittstellen als Protokolle auffaßt, dann kann das Modul, das eine Abstraktion definiert, auch als Protokollkonverter dienen. In vielen Fällen ist dies außerordentlich sinnvoll. Beispiele dafür in der Ulmer Oberon-Bibliothek sind Streams, die bei Verwendung eines Cache Aufrufe zu der jeweiligen Implementierung minimieren, und das Modul Conditions, das aufgrund der zugehörigen Koordinationsaufgabe völlig unterschiedliche Schnittstellen offeriert.

Ein wichtiger Punkt ist die Erweiterbarkeit von Collections. Für das obige Beispiel könnte eine Erweiterung OrderedCollections definiert werden:

DEFINITION OrderedCollections;

   IMPORT Collections, Objects;

   TYPE
      OrderedCollection = POINTER TO OrderedCollectionRec;
      OrderedCollectionRec =
         RECORD
            (Collections.CollectionRec)
         END;

   (* Schnittstelle fuer Implementierungen *)

   TYPE
      Message = RECORD (Collections.Message) END;
      GetMessage =
         RECORD
            (Message)
            index: INTEGER; (* in-parameter *)
            object: Objects.Object; (* out-parameter *)
         END;

   PROCEDURE SetHandler(collection: Collection;
                        handler: Collections.Handler);

   (* Schnittstelle fuer Klienten *)

   PROCEDURE Get(collection: OrderedCollection;
                 index: INTEGER; VAR object: Objects.Object);

END OrderedCollections.

Bei dieser Lösung muß eine Implementierung von OrderedCollections sowohl für Collections als auch für OrderedCollections einen Auftragnehmer deklarieren. Dies läßt sich vermeiden, wenn man gleich im Modul von Collections die Möglichkeit von Erweiterungen in Betracht ziehen würde durch eine Prozedur, die das Versenden beliebiger Erweiterungen von Message an Implementierungen unterstützen würde:

DEFINITION Collections;

   (* ... analog zu oben ... *)

   (* Schnittstelle fuer Erweiterungen *)

   PROCEDURE Send(collection: Collection; VAR message: Message);

END Collections.

Dann benötigt OrderedCollections keine Prozedur SetHandler, sondern könnte mit Collections.Send die Aufträge verschicken. Natürlich gäbe es keine Notwendigkeit für Send, wenn die handler-Komponente bei Collections.Collection öffentlich wäre. Allerdings würden damit die Auftragnehmer der einzelnen Implementierungen öffentlich werden, und es bestünde nicht mehr die Garantie, daß die Auftragnehmer nur eigene Objekte vorfinden würden, d.h. daß ein Laufzeitfehler bei der Typzusicherung, die am Anfang des Auftragnehmers steht, nicht mehr ausgeschlossen werden könnte.

Wenn man sich mit einem einzigen Auftragnehmer begnügt, der nur an einer Stelle angegeben werden muß, dann sind auch Delegationen denkbar, die auch unbekannte Operationen weiterreichen können. Folgendes Beispiel zeigt eine Implementierung von Collection, die alle Operationen an zwei andere Sammlungen weiterreicht. Die Implementierung ist nicht symmetrisch, da nur die Rückgabewerte der ersten Sammlung verwendet werden.

DEFINITION DribbleCollections;

   IMPORT Collections;

   PROCEDURE Create(VAR dribble: Collections.Collection;
                    primary, secondary: Collections.Collection);

END DribbleCollections.

MODULE DribbleCollections;

   IMPORT Collections, Objects;

   TYPE
      DribbleCollection = POINTER TO DribbleCollectionRec;
      DribbleCollectionRec =
         RECORD
            (Collections.CollectionRec)
            primary, secondary: Collections.Collection;
         END;

   PROCEDURE Handler(collection: Collections.Collection;
                     VAR message: Collections.Message);
   BEGIN
      WITH collection: DribbleCollection DO
         Collections.Send(collection.secondary, message);
         (* zuletzt den Auftrag an die Collection weiterreichen,
            die die endgueltigen Rueckgabewerte bestimmen soll
         *)
         Collections.Send(collection.primary, message);
      END;
   END Handler;

   PROCEDURE Create(VAR dribble: Collections.Collection;
                    primary, secondary: Collections.Collection);
      VAR
         collection: DribbleCollection;
   BEGIN
      NEW(collection);
      collection.primary := primary;
      collection.secondary := secondary;
      Collections.SetHandler(collection, Handler);
   END Create;

END DribbleCollections.

Ganz ohne Nebeneffekte verläuft das Verteilen des einen Auftrags an mehrere weitere Implementierungen nicht. Da Aufträge als Variablenparameter übergeben werden, könnte der erste Auftragnehmer den Auftrag in einer Weise verändern, so daß der zweite Auftragnehmer anders reagiert, als wenn er den ursprünglichen Auftrag erhalten hätte. Eine Duplikation der Aufträge ist nicht möglich, da der dynamische Typ eines Auftrags variabel und unbekannt ist.

Ein weiteres Problem ergibt sich bei OrderedCollections.Get. Während es auf den ersten Blick zweckmäßig erscheint, eine Erweiterung von OrderedCollections zu verlangen, ist dies bei möglichen Delegationen hinderlich, da die zwischengelagerten Collections nicht Erweiterungen beliebiger anderer Erweiterungen von Collections.Collection sind. Wenn andererseits OrderedCollections.Get nicht mehr eine OrderedCollection verlangt und sich auch mit einer Collection begnügt, so vergrößert sich natürlich die Gefahr, daß die Empfänger die speziellen Nachrichten der Erweiterungen nicht verstehen und deswegen auch nicht bearbeiten. Erhöhte Flexibilität steht hier im Gegensatz zu der Sicherheit statischer Zusicherungen.


next up previous
Nächste Seite: Operationen ohne zentrale Auftragnehmer Aufwärts: OO-Techniken in Oberon Vorherige Seite: Generische Module
Andreas Borchert 2000-12-18