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

Generische Module

Generische Module wurden zunächst bei CLU eingeführt und etwas später in sehr ähnlicher Form in Ada. Wie Bertrand Meyer gezeigt hat, lassen sich generische Module durch die Anwendung objekt-orientierter Techniken ersetzen, jedoch zumindest im Falle von Eiffel in Verbindung mit einigen Umständlichkeiten. Aus diesem Grund wurden in etwas limitierter Form generische Module sowohl in Eiffel, Modula-3 als auch C++ (hier nennen sie sich templates) eingeführt.

Natürlich handelt es sich bei den Ersatzmechanismen nicht mehr um parametrisierte Module im strengen Sinne. Interessant ist jedoch, ob die Probleme, die zur Einführung generischer Module geführt haben, sich nicht auch in anderer Form elegant lösen lassen. Das klassische Beispiel für generische Module ist Verwaltung von Datenstrukturen, bei denen der Basistyp von untergeordneter Bedeutung ist. Folgendes einfach gehaltene Beispiel zeigt ein allgemeines Modul für Listen in Oberon. Es verwendet als Basistyp Objects.Object, das eine ähnliche Funktion wie ANY in Eiffel übernimmt,2.1jedoch nicht Bestandteil der Sprache Oberon ist.

DEFINITION LinearLists;

   IMPORT Objects;

   TYPE
      List = POINTER TO ListRec;
      ListRec = RECORD (Objects.ObjectRec) END;

   PROCEDURE Create(VAR list: List);
   PROCEDURE Append(list: List; object: Objects.Object);

   PROCEDURE First(list: List);
   PROCEDURE Next(list: List; VAR object: Objects.Object) : BOOLEAN;

END LinearLists.

Create legt eine neue Liste an. Mit Append kann ein neues Element an das Ende der Liste angehängt werden. First und Next erlauben es, die Liste zu durchlaufen. Next liefert dabei FALSE zurück, wenn kein Objekt mehr zurückgeliefert werden kann, da das Ende der Liste erreicht worden ist. In der angebotenen Form ist dies etwas unzureichend, da nur ein Durchlauf zu einem gegebenen Zeitpunkt möglich ist. Später werden Iteratoren in allgemeinerer Form vorgestellt. Die Implementierung könnte folgendermaßen aussehen:

MODULE LinearLists;

   IMPORT Objects;

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

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

   PROCEDURE Create(VAR list: List);
   BEGIN
      NEW(list);
      list.head := NIL; list.tail := NIL; list.cursor := NIL;
   END Create;

   PROCEDURE Append(list: List; object: Objects.Object);
      VAR
         linkable: Linkable;
   BEGIN
      NEW(linkable);
      linkable.object := object;
      linkable.next := NIL;
      IF list.head = NIL THEN
         list.head := linkable;
      ELSE
         list.tail.next := linkable;
      END;
      list.tail := linkable;
   END Append;

   PROCEDURE First(list: List);
   BEGIN
      list.cursor := list.head;
   END First;

   PROCEDURE Next(list: List; VAR object: Objects.Object) : BOOLEAN;
   BEGIN
      IF list.cursor = NIL THEN
         RETURN FALSE
      ELSE
         object := list.cursor.object;
         list.cursor := list.cursor.next;
         RETURN TRUE
      END;
   END Next;

END LinearLists.

Wie sich aus dem Beispiel ersehen läßt, erweist es sich als recht vorteilhaft, mehrere Datentypen in einem Modul vereinbaren zu können. Während bei Eiffel für Linkable eine eigene Klasse mit den zugehörigen Zugriffsoperationen kreiert werden müßte, erlaubt es Oberon, diesen Typ völlig privat zu halten.

Folgendes Beispiel demonstriert, wie auf der Basis von LinearLists eine Liste von Textzeilen aufgebaut und durchlaufen werden kann:

TYPE
   Line = ARRAY 80 OF CHAR;
   LineObject = POINTER TO LineObjectRec;
   LineObjectRec = RECORD (Objects.ObjectRec) line: Line END;

PROCEDURE ReadLines(s: Streams.Stream; VAR list: LinearLists.List);
   (* Lies von dem gegebenen Stream bis zum Auftreten einer
      leeren Zeile oder Eingabeende und lege alle eingelesenen
      Zeilen in der neu zu kreierenden Liste ab
   *)
   VAR
      line: Line;
      lineObject: LineObject;
BEGIN
   LinearLists.Create(list);
   Read.LineS(s, line);
   WHILE (s.count > 0) & (line # "") DO
      NEW(lineObject); lineObject.line := line;
      LinearLists.Append(list, lineObject);
      Read.LineS(s, line);
   END;
END ReadLines;

PROCEDURE WriteLines(s: Streams.Stream; list: LinearLists.List);
   (* Gib alle Zeilen aus, die in der Liste enthalten sind;
      andere Objekte aus der Liste werden ignoriert
   *)
   VAR
      object: Objects.Object;
BEGIN
   LinearLists.First(list);
   WHILE LinearLists.Next(list, object) DO
      IF object IS LineObject THEN
         Write.LineS(s, object(LineObject).line);
      END;
   END;
END WriteLines;

Dieses Beispiel verdeutlicht den Unterschied zu den generischen Modulen aus CLU und Ada. Während bei CLU und Ada der Elementtyp der Liste genau festgelegt werden würde und damit statisch überprüfbar wäre, liegt hier nur fest, daß alle Elementtypen Erweiterungen von Objects.Object sein müssen. Dies liefert sowohl die Freiheit, heterogene Objekte in einer Liste zu verwalten, als auch die offene Frage, welche Sorten von Objekten man später darin wiederfindet.

Eiffel, Modula-2 und C++ legen natürlich den Elementtyp nicht so fest wie CLU und Ada, da sie auch Erweiterungen akzeptieren. Im Gegensatz zur obigen Lösung liefern sie vollständige statische Überprüfbarkeit auf Kosten eines nicht trivialen Sprachmittels. Allerdings ist dies bei Eiffel nicht vollständig gelungen: So zeigt Cook neben einigen Problemen im Typsystem von Eiffel auch eines in Zusammenhang mit generischen Klassen auf, das - wenn überhaupt - nur zur Laufzeit entdeckt werden kann.

Wenn im obigen Beispiel WriteLines absolut sichergehen kann, daß alle Objekte der Liste Erweiterungen von LineObject sind, dann kann der Programmtext weiter vereinfacht werden:

PROCEDURE WriteLines(s: Streams.Stream; list: LinearLists.List);
   (* Gib alle Zeilen aus, die in der Liste enthalten sind;
      es wird davon ausgegangen, dass alle Elemente der Liste
      Erweiterungen von LineObject sind
   *)
   VAR
      lineObject: LineObject;
BEGIN
   LinearLists.First(list);
   WHILE LinearLists.Next(list, lineObject) DO
      Write.LineS(s, lineObject.line);
   END;
END WriteLines;

Dies ist erlaubt, da LineObject eine Erweiterung von Objects.Object ist und damit direkt an LinearLists.Next übergeben werden kann. In diesem Fall würde es innerhalb von LinearLists.Next zu einem Laufzeitfehler bei der Zuweisung führen, falls das zugewiesene Element doch keine Erweiterung von LineObject ist.

Ada und CLU offerieren zusätzlich die Möglichkeit, Prozeduren und Variablen als Parameter bei generischen Modulen anzugeben. Dies ist beispielsweise sinnvoll bei einem allgemeinen Sortiermodul, das eine Vergleichsprozedur benötigt. In Oberon kann hierfür der Basistyp so definiert werden, daß zu ihm eine Sortierprozedur gehört (Techniken dazu folgen), oder es kann mit konventioneller Technik analog zu Modula-2 ein Prozedurparameter explizit angegeben werden.


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