next up previous
Nächste Seite: Klassenbezogene Ereignisse Aufwärts: Ausnahmenbehandlungen Vorherige Seite: Objekt-orientierte Ausnahmenbehandlung

Objektbezogene Ereignisse

Angesichts der Defizite bei globalen Ereignistypen für Ausnahmenbehandlungen und der Attraktivität von Ausnahmenbearbeitern, die direkt mit Objekten verknüpft werden, liegt es in Oberon nahe, Ereignisse bei Bedarf in Bezug zu einem Objekt zu setzen.

Eine einfache Technik wäre die Definition eines individuellen Ereignistyps für jedes Objekt:

TYPE
   Object = POINTER TO ObjectRec;
   ObjectRec =
      RECORD
         eventType: Events.EventType;
      END;

Dies würde es erlauben, einen Interessenten für Ereignisse, die ein bestimmtes Objekt betreffen, zu definieren. Bei Ausnahmen, die in Zusammenhang mit einem Objekt entstehen, würde dann der objekt-spezifische Ereignistyp verwendet werden. Diese Technik hat allerdings folgende Nachteile:

All diese Nachteile werden in der Ulmer Oberon-Bibliothek durch das Modul RelatedEvents behoben. Dieses Modul erwartet von den Objekten nur, daß sie eine Erweiterung von Disciplines.Object sind. Damit ist RelatedEvents in der Lage, seine eigene Datenstruktur zu einem Objekt zu unterhalten, und es gibt keinen erhöhten Initialisierungsaufwand. Nur wenn es zu objektbezogenen Ereignissen kommt oder wenn besondere Maßnahmen zu treffen sind, ist RelatedEvents gezwungen, private Informationen zu einem Objekt zu verwalten.

Auf der Seite derjenigen, die Ereignisse generieren, ist der Unterschied minimal. Statt Events.Raise wird RelatedEvents.Raise aufgerufen, das als zusätzlichen Parameter ein Objekt akzeptiert:

DEFINITION RelatedEvents;

   IMPORT Disciplines, Events;

   (* ... *)

   PROCEDURE Raise(object: Disciplines.Object; event: Events.Event);

END RelatedEvents.

Bemerkenswert ist, daß diese Technik keine zusätzlichen Parameter bei Operationen benötigt, die möglicherweise eine Ausnahmenbehandlung initiieren. Statt dessen wird einfach der Parameter genommen, auf den sich die Operation bezieht:

MODULE SomeModule; (* revidierte Version auf Basis von RelatedEvents *)

   (* ... *)

   PROCEDURE Error(object: Disciplines.Object; code: INTEGER; ...);
      VAR
         event: ErrorEvent;
   BEGIN
      NEW(event); event.type := error;
      event.message := (* etwas in Abhaengigkeit von `code' *)
      event.code := code;
      (* Ausfuellen weiterer beschreibender Komponenten *)
      RelatedEvents.Raise(object, event);
   END Error;

   PROCEDURE SomeOperation(object: ...; ...) : BOOLEAN;
   BEGIN
      (* ... *)
      IF (* Ausfall entdeckt *) THEN
         Error(object, ...); RETURN FALSE
      END;
      (* ... *)
      RETURN TRUE
   END SomeOperation;

BEGIN
   Events.Define(error);
END SomeModule.

Außerdem offeriert RelatedEvents vier Reaktionsmöglichkeiten auf objektbezogene Ereignisse, die auch kombiniert benutzt werden können:

Wird keine der angegebenen Möglichkeiten gewählt, so werden Ereignisse an Events.Raise weitergeleitet. Wenn also keine Vorkehrungen getroffen worden sind, um objektbezogene Ereignisse zu bearbeiten, dann führen sie per Voreinstellung zum Abbruch des Programms. Natürlich ist es möglich, pauschal Events.Ignore für den zugehörigen Ereignistyp aufzurufen oder globale Interessenten anzugeben. Dies ist jedoch eher die Ausnahme.

Folgendes Beispiel zeigt, wie sich auf dieser Basis Ausnahmenbehandlungstechniken realisiert werden können, die dem Ebenenmodell entsprechen:

PROCEDURE ClientOfSomeOperation(object: ...; ...) : BOOLEAN;
   VAR
      queue, error: RelatedEvents.Queue;
BEGIN
   RelatedEvents.QueueEvents(object);
   (* ... *)
   IF ~SomeOperation(object, ....) OR
         ~AnotherOperation(object, ...) THEN
      RelatedEvents.GetQueue(object, queue);
      error := queue;
      WHILE error \# NIL DO
         IF error.event IS SomeModule.ErrorEvent THEN
            (* ... *)
         ELSIF error.event IS AnotherModule.ErrorEvent THEN
            (* ... *)
         END;
         error := error.next;
      END;
      (* Entscheidung darueber, ob man es erneut probiert oder
         einen Ausfall signalisiert
      *)
      IF (* Ausfall *) THEN
         RelatedEvents.AppendQueue(object, queue);
         Error(object, ...); RETURN FALSE
      END;
   END;
   (* ... *)
END ClientOfSomeOperation;

Mit RelatedEvents.QueueEvents wird sichergestellt, daß alle Ereignisse in einer objektbezogenen Liste gesammelt werden. Sollte QueueEvents bereits vorher aufgerufen worden sein, so bewirkt dies keine Änderung. Mit RelatedEvents.GetQueue kann diese Liste von dem Objekt entfernt und untersucht werden. Möchte man später die Liste wieder an das Objekt hängen, um sie auch übergeordneten Klienten zur Verfügung zu stellen, so kann dies durch RelatedEvents.AppendQueue geschehen.

Bei traditionellen Techniken kann entweder die ursprüngliche Ausfallbeschreibung oder eine neue Beschreibung weitergeleitet werden - jedoch nicht beides. Mit Hilfe von RelatedEvents.QueueEvents können alle Ausfallbeschreibungen erhalten und später untersucht werden. Dabei handelt es sich in der Regel nicht um Verletzung von Abstraktionsgrenzen. Im Gegenteil, durch die Verwendung von up calls können Ausnahmen des Moduls C von größerem Interesse für A sein als für B. Während beispielsweise für Write.Int und Streams der exakte Grund für einen Schreibfehler uninteressant ist, so ist auf der höheren Ebene durchaus bekannt, daß es sich um eine Datei auf einem Dateisystem des Betriebssystems handelt, die z.B. von einer vollen Platte bedroht sein kann. Statt einem pauschalen ``write error'' kann dann dem Benutzer eine aussagefähige Fehlermeldung gegeben werden.

Auch objekt-orientierte Ausnahmenbearbeiter können mit Hilfe von RelatedEvents definiert werden. Das vorgestellte Beispiel für die Erweiterung von Ada könnte etwa folgendermaßen in Oberon aussehen:

DEFINITION Stacks;

   IMPORT Disciplines, Events, Objects;

   CONST
      overflow = 0; noSpace = 1;
   TYPE
      ErrorEvent = POINTER TO ErrorEventRec;
      ErrorEventRec =
         RECORD
            (Events.EventRec)
            code: INTEGER; (* overflow oder noSpace *)
            fixed: BOOLEAN; (* Problem durch Ausnahmenbehandlung behoben? *)
         END;
   VAR
      error: Events.EventType;

   TYPE
      Stack = POINTER TO StackRec;
      StackRec = RECORD (Disciplines.ObjectRec) END;

   PROCEDURE Create(VAR stack: Stack; tentativeSizeLimit: INTEGER) : BOOLEAN;

   PROCEDURE Push(stack: Stack; element: Objects.Object) : BOOLEAN;

   (* ... *)

   PROCEDURE Expand(stack: Stack; amount: INTEGER) : BOOLEAN;

END Stacks.

Das Beispiel verlangt die Möglichkeit, die Ausführung einer Operation fortzusetzen, nachdem eine Ausnahmenbehandlung initiiert worden ist. Aus diesem Grund wurde Stacks.EventRec um die Komponente fixed erweitert. Entsprechend kann Stacks nach Events.Raise überprüfen, ob fixed auf TRUE gesetzt worden ist, und dann einen erneuten Versuch unternehmen.

   PROCEDURE ExpandingHandler(event: Events.Event);
      VAR
         stackevent: Stacks.ErrorEvent;
   BEGIN
      WITH event: RelatedEvents.Event DO
         stackevent := event.event(Stacks.ErrorEvent);
         IF stackevent.code = Stacks.overflow THEN
            stackevent.fixed := Stacks.Expand(event.object, growthRate);
         END;
      END;
   END ExpandingHandler;

   PROCEDURE CreateExpandingStack(VAR stack: Stacks.Stack) : BOOLEAN;
      VAR
         stack: Stacks.Stack;
         eventType: Events.EventType;
   BEGIN
      IF Stacks.Create(stack, 20) THEN
         RelatedEvents.GetEventType(stack, eventType);
         Events.Handler(eventType, ExpandingHandler);
         RETURN TRUE
      ELSE
         RETURN FALSE
      END;
   END CreateExpandingStack;

ExpandingHandler findet hier zunächst das Ereignis von RelatedEvents vor, das nur die Verweise auf das eigentliche Ereignis und das Objekt enthält. Wenn ExpandingHandler dann feststellt, daß es sich um ein ``overflow''-Ereignis handelt, wird versucht, den Keller zu expandieren. Nur wenn dies gut geht, wird fixed auf TRUE gesetzt und ein erneuter Versuch ermöglicht.

CreateExpandingStack versucht zunächst, einen Keller anzulegen, und erfährt dann durch RelatedEvents.GetEventType den zum Objekt gehörenden Ereignistyp, für den ExpandingHandler als Interessent angegeben wird.

RelatedEvents.GetEventType und RelatedEvents.QueueEvents sind kombinierbar. Wenn Stacks.Push vor einem erneuten Versuch das Ereignis nicht mit RelatedEvents.GetQueue entfernt, steht es nachher auch dann zur Verfügung, wenn der zweite Versuch erfolgreich ist. Bemerkenswert ist hier, daß beliebig viele Module ihr Interesse an Ereignissen an einem bestimmten Objekt bekunden können.

Die gezeigten Techniken sind in der Praxis jedoch relativ selten notwendig. Häufig ist es nur nötig, die Historie über die Ausfälle beim Objekt aufzubewahren, um eine sinnvolle Fehlermeldung zusammenzustellen, die die komplette Geschichte erzählt. Da die message-Komponente mit ihrer Begrenzung auf 80 Zeichen manchmal unzureichend für eine textuelle Problembeschreibung ist, gibt es eine Möglichkeit, Ausgabeprozeduren mit Ereignistypen zu assoziieren:

PROCEDURE PrintError(s: Streams.Stream; event: Events.Event);
BEGIN
   WITH event: ErrorEvent DO
      (* Ausgabe einer detaillierten Fehlermeldung auf `s' *)
   END;
END PrintError;

PROCEDURE SetupErrorHandling;
   (* wird waehrend der Initialisierung des Moduls aufgerufen *)
BEGIN
   Events.Define(error);
   Errors.AssignWriteProcedure(error, PrintError);
END SetupErrorHandling;

Errors.AssignWriteProcedure nutzt hierbei aus, daß Events.EventType eine Erweiterung von Disciplines.Object ist, und deponiert die Prozedur mit Hilfe einer Disziplin. Später kann dann mit Errors.Write eine Ausgabe eines Ereignisses erfolgen. Wurde für das Ereignis Errors.AssignWriteProcedure vorher nicht aufgerufen, so wird statt dessen die message-Komponente ausgegeben.

Ein einfaches Kopierkommando für UNIX-Dateien, das die Fehlermeldungen an den Benutzer selbst ausgeben soll, könnte dann folgendermaßen aussehen:

PROCEDURE Copy(infile, outfile: ARRAY OF CHAR);
   VAR
      errors: Disciplines.Object; (* Dummy-Objekt fuer die Ausnahmenbehandlung *)
      infile, outfile: NameSpaces.Name;
      in, out: Streams.Stream;
BEGIN
   NEW(errors); RelatedEvents.QueueEvents(errors);
   IF ~UnixFiles.Open(in, infile, UnixFiles.read, errors) OR
         ~UnixFiles.Open(out, outfile,
                         UnixFiles.write + UnixFiles.create, errors) THEN
      Conclusions.Conclude(errors, Errors.error, "Copy");
      RETURN
   END;
   RelatedEvents.Forward(in, errors);
   RelatedEvents.Forward(out, errors);
   IF ~Streams.Copy(in, out) OR
         ~Streams.Close(in) OR ~Streams.Close(out) THEN
      Conclusions.Conclude(errors, Errors.error, "Copy");
   END;
END Copy;

Da bei Konstruktoren ein Bezugsobjekt noch nicht zur Verfügung steht, ist hier ein zusätzlicher Parameter notwendig, um eine Ausnahmenbehandlung zu ermöglichen. Hierfür verwendet man ein ``leeres'' Objekt vom Typ Disciplines.Object, das nur dazu dient, die Fehler aufzufangen. Da bei Streams.Copy Ausfälle sich sowohl auf in als auch out beziehen können, erweist es sich hier als günstig, die Ereignisse bei beiden Objekten an errors weiterzuleiten. Conclusions.Conclude gibt dann die Liste der Ereignisse in zusammengestellter Form auf Streams.stderr aus.

Im obigen Beispiel könnte eingewendet werden, daß bei einem Ausfall von Streams.Copy weder in noch out geschlossen werden. Dies läßt sich dadurch beheben, daß man alle drei Operationen hintereinander ausführt und erst anschließend überprüft, ob es zu Ausfällen gekommen ist:

   IF ~Streams.Copy(in, out) THEN (* Ausfall zunaechst ignorieren *) END;
   Streams.Release(in); (* aequivalent zu Streams.Close ohne Rueckgabewert *)
   Streams.Release(out);
   IF RelatedEvents.EventsPending(errors) THEN
      Conclusions.Conclude(errors, Errors.error, "Copy");
   END;

RelatedEvents.EventsPending liefert TRUE zurück, wenn die Liste der zum Objekt gehörenden Ereignisse nicht leer ist. Und genau in diesem Fall wird eine Fehlermeldung ausgegeben.


next up previous
Nächste Seite: Klassenbezogene Ereignisse Aufwärts: Ausnahmenbehandlungen Vorherige Seite: Objekt-orientierte Ausnahmenbehandlung
Andreas Borchert 2000-12-18