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