next up previous
Nächste Seite: Disziplinen Aufwärts: OO-Techniken in Oberon Vorherige Seite: Aufträge und Auftragnehmer

Operationen ohne zentrale Auftragnehmer

Der vorgestellte Ansatz ist zwar recht flexibel bis zur Unterstützung von Delegation selbst unbekannter Operationen, jedoch verbleiben einige Nachteile:

Das war einer der Beweggründe für Mössenböck, typgebundene Prozeduren, die in etwa den Methoden aus den klassischen objekt-orientierten Sprachen entsprechen, in Oberon-2 einzuführen. Allerdings läßt sich dieses Problem auch in Oberon lösen durch die Einführung von Schnittstellenrecords. Wie sich zeigt, geben sie die notwendige Sicherheit und Effizienz und weisen sogar einige Vorteile gegenüber der klassischen Lösung auf.

Als Beispiel läßt sich die Abstraktion für Collections nehmen:

DEFINITION Collections;

   IMPORT Disciplines, Objects;

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

   (* Schnittstelle fuer Implementierungen *)

   TYPE
      AddProc = PROCEDURE (collection: Collection; object: Objects.Object);
      FirstProc = PROCEDURE (collection: Collection);
      NextProc = PROCEDURE (collection: Collection;
                            VAR object: Objects.Object) : BOOLEAN;
      Interface = POINTER TO InterfaceRec;
      InterfaceRec =
         RECORD
            add: AddProc;
            first: FirstProc;
            next: NextProc;
         END;

   PROCEDURE Init(collection: Collection; if: Interface);

   (* 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.

Collections.Add würde dann folgendermaßen aussehen:

TYPE
   Collection = POINTER TO CollectionRec;
   CollectionRec =
      RECORD
         (Disciplines.ObjectRec)
         (* private Komponenten *)
         if: Interface;
      END;

(* ... *)

PROCEDURE Add(collection: Collection; object: Objects.Object);
BEGIN
   collection.if.add(collection, object);
END Add;

Bei einer Implementierung von Collections wird damit der zentrale Auftragnehmer überflüssig und die der Operation entsprechende Prozedur wird direkt aufgerufen. Zudem besteht die Sicherheit, daß alle Operationen auch bekannt sind. Nur wenn die Implementierung vergißt, Collections.Init beim Anlegen eines neuen Objekts aufzurufen, oder nicht alle Prozedurkomponenten initialisiert, kann es zu Laufzeitfehlern kommen. Allerdings haben solche Fehler kaum eine Chance, einen allerersten Test zu überstehen.

Wie bereits erwähnt wurde, hat das Einschieben eines Moduls zwischen dem Klienten und der eigentlichen Implementierung sowohl Vor- als auch Nachteile. Ein Vorteil besteht darin, daß beispielsweise eine Implementierung erklären kann, daß sie nicht alle Operationen unterstützt. Wie bereits im Beispiel des iostream bei C++ erläutert wurde, ist es nicht sinnvoll, für jede mögliche Variante eine eigene Klasse zu definieren. Statt dessen sind neben einigen Pflichtoperationen eine Reihe von optionalen Fähigkeiten denkbar, die dann auch von den Klienten überpüfbar sind, so daß Überraschungen vermieden werden können. Beim Beispiel der Collections könnte man beispielsweise OrderedCollections in die Basisabstraktion integrieren:

DEFINITION Collections;

   IMPORT Disciplines, Objects;

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

   (* Schnittstelle fuer Implementierungen *)

   TYPE
      AddProc = PROCEDURE (collection: Collection; object: Objects.Object);
      FirstProc = PROCEDURE (collection: Collection);
      NextProc = PROCEDURE (collection: Collection;
                            VAR object: Objects.Object) : BOOLEAN;
      GetProc = PROCEDURE (collection: Collection;
                           index: INTEGER; VAR object: Objects.Object);
      SetProc = PROCEDURE (collection: Collection;
                           index: INTEGER; object: Objects.Object);
      Interface = POINTER TO InterfaceRec;
      InterfaceRec =
         RECORD
            add: AddProc;
            first: FirstProc;
            next: NextProc;
            get: GetProc;
            set: SetProc;
         END;
   CONST
      get = 0; set = 1;
   TYPE
      Capability = SHORTINT; (* get..set *)
      CapabilitySet = SET; (* OF Capability *)

   PROCEDURE Init(collection: Collection; if: Interface; caps: CapabilitySet);

   (* Schnittstelle fuer Klienten *)

   PROCEDURE Capabilities(collection: Collection) : CapabilitySet;

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

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

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

END Collections.

Damit gehören Add, First und Next zu den Pflichtoperationen, während Get und Set optional sind. Die Prozedur Capabilities erlaubt es Klienten, nachzufragen, welche der optionalen Fähigkeiten vorhanden sind. Collections.Get und Collections.Set würden dann explizit das Vorhandensein der entsprechenden Fähigkeit überprüfen - die Notwendigkeit von Dummy-Prozeduren auf der Seite der Implementierung entfällt:

TYPE
   Collection = POINTER TO CollectionRec;
   CollectionRec =
      RECORD
         (Disciplines.ObjectRec)
         (* private Komponenten *)
         if: Interface;
         caps: CapabilitySet;
      END;

(* ... *)

PROCEDURE Get(collection: Collection;
              index: INTEGER; VAR object: Objects.Object);
BEGIN
   IF get IN collection.caps THEN
      collection.if.get(collection, index, object);
   ELSE
      (* Laufzeitfehler *)
   END;
END Get;

Es wäre auch denkbar, alle Operationen der Schnittstelle in die Menge der Fähigkeiten aufzunehmen, auch wenn einige davon nicht optional sind. Collections.Init könnte dann überprüfen, ob die Menge der Pflichtoperationen dabei ist. Diese Technik würde einer möglichen Implementierung mehr Sicherheit geben, da das Übereinstimmen der zugewiesenen Operationen beim Schnittstellenrecord mit der Menge der Fähigkeiten sich leicht lokal überprüfen läßt. Wenn später Collections um eine weitere Pflichtoperation erweitert wird, führt dies sofort zu einem Laufzeitfehler, selbst wenn die neue Pflichtoperation noch nicht benutzt wird.

Collections als Mittler zwischen Klienten und Implementierungen kann auch beiden Seiten individuell entgegenkommen, um einerseits Umständlichkeiten zu ersparen und andererseits besondere Effizienzsteigerungen zu ermöglichen. Ein Beispiel wäre das Hinzufügen nicht nur von einzelnen Elementen, sondern kompletten Sammlungen. Die vorgestellten Varianten würden in jedem Fall dem Aufrufer die Last aufbürden, eine Schleife zu schreiben, die die Elemente einzeln von der einen Sammlung in die andere befördert. Dabei könnten zumindest manche Implementierungen davon profitieren, wenn sie wüßten, wieviele Elemente auf einmal einzufügen sind. Andererseits wäre es natürlich sehr umständlich, wenn jede Implementierung eine Operation unterstützen müßte, die es erlaubt, vollständige Sammlungen hinzuzufügen. Folgende Änderung von Collections zeigt, daß man beide Seiten zufriedenstellen kann:

DEFINITION Collections;

   IMPORT Disciplines, Objects;

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

   (* Schnittstelle fuer Implementierungen *)

   TYPE
      AddProc = PROCEDURE (collection: Collection; object: Objects.Object);
      AddCollectionProc = PROCEDURE (collection, objects: Collection);
      (* ... weitere Prozedurtypen analog zu oben ... *)
      Interface = POINTER TO InterfaceRec;
      InterfaceRec =
         RECORD
            add: AddProc;
            addcollection: AddCollectionProc;
            first: FirstProc;
            next: NextProc;
            get: GetProc;
            set: SetProc;
         END;
   CONST
      addcollection = 0; get = 1; set = 2;
   TYPE
      Capability = SHORTINT; (* addcollection..set *)
      CapabilitySet = SET; (* OF Capability *)

   PROCEDURE Init(collection: Collection; if: Interface; caps: CapabilitySet);

   (* Schnittstelle fuer Klienten *)

   PROCEDURE Capabilities(collection: Collection) : CapabilitySet;

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

   PROCEDURE AddCollection(collection: Collection; objects: Collection);

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

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

END Collections.

Collections.AddCollection würde dann folgendermaßen aussehen:

PROCEDURE AddCollection(collection: Collection; objects: Collection);
   VAR
      object: Objects.Object;
BEGIN
   IF addcollection IN collection.caps THEN
      collection.if.addcollection(collection, objects);
   ELSE
      First(objects);
      WHILE Next(objects, object) DO
         Add(collection, object);
      END;
   END;
END AddCollection;

Dies entspricht effektiv der Technik des Überdefinierens von Methoden bei klassischen objekt-orientierten Sprachen. Diese lassen sich auch bei Erweiterungshierarchien sukzessiv überdefinieren. So könnte eine Erweiterung von Collections bei der Initialisierung einer Sammlung die Initialisierung der Basisabstraktion übernehmen und dabei optional einzelne Operationen selbst definieren.

Zwar liefert diese Technik einen Zuwachs an Sicherheit - es bleibt jedoch die Frage, ob sich Delegationen immer noch realisieren lassen. Zunächst ist es sicherlich kein Problem, die Operationen der Basisabstraktion durchzureichen, wie die angepaßte Version der DribbleCollections zeigt:

MODULE DribbleCollections;

   IMPORT Collections, Objects;

   TYPE
      DribbleCollection = POINTER TO DribbleCollectionRec;
      DribbleCollectionRec =
         RECORD
            (Collections.CollectionRec)
            primary, secondary: Collections.Collection;
            secondaryCaps: Collections.CapabilitySet;
         END;
   VAR
      if: Collections.Interface;

   PROCEDURE Add(collection: Collections.Collection;
                 object: Objects.Object);
   BEGIN
      WITH collection: DribbleCollection DO
         Collections.Add(primary, object);
         Collections.Add(secondary, object);
      END;
   END Add;

   (* analog First, Next, Set und Get,
      wobei bei First, Next und Get darauf verzichtet werden kann,
      die Operation auch fuer secondary durchzufuehren
   *)

   PROCEDURE Init;
   BEGIN
      NEW(if);
      if.add := Add; if.first := First; if.next := Next;
      if.get := Get; if.set := Set;
   END Init;

   PROCEDURE Create(VAR dribble: Collections.Collection;
                    primary, secondary: Collections.Collection);
      VAR
         collection: DribbleCollection;
         caps: Collections.CapabilitySet;
   BEGIN
      NEW(collection);
      collection.primary := primary;
      collection.secondary := secondary;
      (* wir unterstuetzen hier die Schnittmenge der Faehigkeiten
         von beiden Sammlungen
      *)
      caps := Collections.Capabilities(primary) *
              Collections.Capabilities(secondary);
      Collections.Init(collection, if, caps);
   END Create;

BEGIN
   Init;
END DribbleCollections.

Bemerkenswert ist hier das Konzept der Fähigkeiten. DribbleCollections kann sich individuell an die gegebenen Sammlungen anpassen. Im Beispiel wird die Schnittmenge verwendet. Es wäre natürlich auch denkbar, genau die Fähigkeiten von primary durchzureichen und die Operationen bei secondary nur soweit nachzuziehen, wie sie unterstützt werden. Der Grund für die Trennung des Schnittstellenrecords von der Menge der Fähigkeiten ergibt sich ganz deutlich: Der Schnittstellenrecord bleibt konstant und braucht nur ein einziges Mal angelegt zu werden, während die Menge der Fähigkeiten von Fall zu Fall variieren kann.

Wenn die Operationen unbekannter Erweiterungen unterstützt werden sollen, dann bleibt nichts anderes übrig, als dafür wieder das Konzept der Aufträge einzuführen. Allerdings dürften die Operationen der Erweiterungen möglicherweise nicht so häufig auftreten wie die der Basisabstraktion, so daß sich der damit verbundene Aufwand in Grenzen hält. So könnte Collections folgendermaßen ergänzt werden:

DEFINITION Collections;

   (* ... *)

      Message = RECORD END;
      HandlerProc = PROCEDURE (collection: Collection; VAR message: Message);

      Interface = POINTER TO InterfaceRec;
      InterfaceRec =
         RECORD
            add: AddProc;
            addcollection: AddCollectionProc;
            first: FirstProc;
            next: NextProc;
            get: GetProc;
            set: SetProc;
            handler: HandlerProc;
         END;
   CONST
      addcollection = 0; get = 1; set = 2; handler = 3;
   TYPE
      Capability = SHORTINT; (* addcollection..handler *)
      CapabilitySet = SET; (* OF Capability *)

   (* ... *)

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

END Collections.

Ein Auftragnehmer braucht nur dann von einer Implementierung definiert zu werden, wenn sie eine Erweiterung unterstützt, die Aufträge verschickt, oder wenn wie etwa im Beispiel von DribbleCollections unbekannte Operationen weitergereicht werden sollen.


next up previous
Nächste Seite: Disziplinen Aufwärts: OO-Techniken in Oberon Vorherige Seite: Aufträge und Auftragnehmer
Andreas Borchert 2000-12-18