Ausschnitt aus der Ada-Version des Buchs Objektorientiertes Plug und Play
© Prof. Dr. Andreas Solymosi

Mengen

Viele Datenbehälter sind geeignet, um einen Wert (ein Datenelement) zu speichern. Das Beschreiben eines solchen Datenbehälters bewirkt, daß sein Inhalt unwiderruflich verloren geht. Somit "erinnert sich" ein Datenbehälter nur an seine unmittelbare Vergangenheit, nämlich an den Parameter des letzten Mutatoraufrufs. Solche Behälter nennen wir Unibehälter.

Es gibt Datenbehälter, die mehrere Werte aufnehmen können. Diese heißen dementsprechend Multibehälter. Der Aufruf eines Mutators, der einen Wert in einen Multibehälter überträgt, überschreibt nicht (unbedingt) seinen vorhandenen Inhalt. Mit Hilfe von Informatoren können die gespeicherten Elemente gelesen werden. Für das Löschen von vorhandenen Daten gibt es gesonderte Mutatoren.

Bei vielen Unibehältern beinhaltet also die Operation Schreiben ein implizites Loeschen . Die Folge dieser Eigenschaft ist, daß Unibehälter nur einen Wert (ein Datum) behalten können. Wenn ein weiterer Wert hineingeschrieben wird, geht sein Inhalt ersatzlos und ohne Warnung verloren. Ein Wert kann jedoch mehrfach gelesen werden; somit ist die Vermehrung von Daten - zum Leidwesen vieler Softwarefirmen - frei.

Wie für Unibehälter, gibt es auch für Multibehälter Operationen, die nicht an einem, sondern an zwei Behältern arbeiten. Ein Beispiel dafür ist die Abfrage, ob zwei Behälter in irgendwelchem Sinne gleich sind. Meistens versteht man darunter, ob die beiden den gleichen Inhalt haben. Oft wird diese Operation auch hier mit dem Gleichheitszeichen "=" benannt, und heißt auch Gleichheit. Eine andere, ähnliche Operation, die den Inhalt von zwei Behältern gleichsetzt, ist das Erzwingen der Gleichheit. Eine Möglichkeit dafür ist die bekannte (gefährliche) Zuweisung, die den Inhalt des einen löscht und den Inhalt des anderen hineinkopiert. Die Gefahren dieser von der Sprache gelieferten, "geschmierten" Operationen können besonders für Multibehälter Überraschungen bereiten und werden deswegen meistens unterbunden, indem die Datentypen für Multibehälter als eingeschränkt privat vereinbart werden.

Beispiele für Multibehälter sind Mengen. Eine Menge ist eine Sammlung von Elementen, wobei jedes Element nur einmal in einer Menge enthalten sein kann. Ein weiteres Eintragen desselben Elements verändert die Menge nicht. Das Entfernen eines nicht vorhandenen Elements verändert die Menge ebenfalls nicht. Über Mengen können oft die bekannten mengentheoretischen Operationen wie Vereinigung, Schnitt und Komplement durchgeführt werden:

Der abstrakte Datentyp TMenge wird von der Ausprägung der Paketschablone GMenge exportiert. Seine Objekte müssen mit dem Konstruktor Entleeren in einen definierten Zustand gebracht werden. Zum Datentyp gehören die Mutatoren Fuellen und Entfernen sowie der Informator Vorhanden. Die Schablone muß mit dem Aufzählungstyp TElement ausgeprägt werden. Alle drei Operationen haben einen Schreibparameter vom Typ TMenge und einen Leseparameter vom Typ TElement.

Charakteristisch für alle bis jetzt kennengelernten Datenobjekte ist ihre Eigenschaft, daß sie bei jeder Programmausführung mit Daten versorgt werden müssen. Die eingetragenen Daten gehen bei der Beendigung des Programms verloren. Für Multibehälter ist es besonders unangenehm: Sie werden aufwendig gefüllt, da sie viele Daten enthalten.

Es ist jedoch vorstellbar, den Inhalt einer Menge mit Hilfe von Operationen wie Speichern auf einen externen Datenträger zu retten. Beim nächsten Programmaufruf besteht dann die Möglichkeit, diese Daten mit Hilfe eines Operationsaufrufs wie Laden wieder in den Datenbehälter zu laden. Diese nennen wir Persistenzoperationen.

Der obige Abschnitt beschreibt im wesentlichen die Schnittstelle von GMenge verbal. Formal sieht dies folgendermaßen aus:

generic -- Ausschnitt aus der Datei GMemge.ads
	type TElement is private; -- ausprägbar einem beliebigen Datentyp außer limited private
	with procedure Element_anzeigen (Element: in TElement); -- zeigt ein Element am Bildschirm an
package GMenge is
	type TMenge is limited private;
	procedure Entleeren (Menge: out TMenge); -- Konstruktor, löscht den gesamten Inhalt
	procedure Fuellen (Menge: in out TMenge; Element: in TElement); -- trägt Element in Menge ein
	procedure Entfernen (Menge: in out TMenge; Element: in TElement); -- löscht Element aus Menge
	function Vorhanden (Menge: TMenge; Element: TElement) return Boolean;
	function Leer (Menge: TMenge) return Boolean;
	function Voll (Menge: TMenge) return Boolean;
	function "or" (Links, Rechts: TMenge) return TMenge; -- bildet die Vereinigung zweier Mengen
	function "and" (Links, Rechts: TMenge) return TMenge; -- bildet den Schnitt zweier Mengen
	function "not" (Rechts: TMenge) return TMenge; -- bildet das Komplement einer Menge
	procedure Kopieren (Ziel: out TMenge; Quelle: in TMenge); -- ersetzt die Zuweisung
	function "=" (Links, Rechts: TMenge) return Boolean;
	procedure Speichern (Menge: in TMenge; Dateiname: in String); -- Persistenzoperationen
	procedure Laden (Menge: out TMenge; Dateiname: in String); -- raises:
	EDatei: exception; -- der Inhalt der Datei paßt nicht in Menge, oder keine Datei mit Dateiname
	procedure Alles_anzeigen (Menge: in TMenge); -- ruft Element_anzeigen für alle eingetragenen Elemente auf
private ...
end GMenge;

Den exportierten abstrakten Datentyp TMenge nennen wir in diesem Fall Schablonentyp; er heißt auch generischer Datentyp oder manchmal Datentypschablone. TElement ist der formale Ausprägungsparameter der Paketschablone GMenge. Bei der Ausprägung wird an ihrer Stelle ein aktueller Ausprägungsparameter eingesetzt. TElement ist hier ein formaler Ausprägungstyp.

Die Ausprägungsprozedur Anzeigen ist deswegen nötig, weil nur der Benutzer (der den Datentyp TElement definiert) weiß, wie ein Datenobjekt dieses Typs am Bildschirm angezeigt werden kann. Dies ist eine Rückrufprozedur, die von der Operation Alles_anzeigen für jedes eingetragene Element aufgerufen wird.

Möchte zum Beispiel der Benutzer diese Modulschablone für den von ihm definierten Datentyp

type TTier is (Elefant, Tiger, Loewe, Giraffe, Antilope);

ausprägen, dann weiß nur er, wie ein Tier auf dem Bildschirm dargestellt werden kann. Daher muß er eine Prozedur

procedure Tier_malen (Tier: in TTier);

vereinbaren. Sie muß der Ausprägung übergeben werden:

package MTier_Menge is new GMenge (TElement => TTier, Element_anzeigen => Tier_malen);

Nun kann er in seinem Safari-Program, in dem Buch über die geschossenen Tierarten geführt werden soll, eine Menge aus Tieren anlegen:

Tiermenge: MTier_Menge.TMenge;
Geschossen: TTier;
	...
MTier_Menge.Fuellen (Menge => Tiermenge, Element => Geschossen);

Ruft er nun die Operation Alles_anzeigen auf, ruft diese seine Prozedur Tier_malen für jede Tierart zurück, die in die Tiermenge eingetragen wurde.

Ein Nachteil dieser Vorgehensweise ist, daß jeder Benutzer von GMenge eine Rückrufprozedur vereinbaren muß, um die Schablone ausprägen zu können. Diejenigen, die Alles_anzeigen gar nicht aufrufen wollen, müssen eine Attrappe definieren: Eine Prozedur, die gar nichts tut, auch nicht aufgerufen wird. Sie wird nur benutzt, als aktueller Schablonenparameter eingesetzt zu werden:

procedure Attrappe (Tier: in TTier) is begin null; end Attrappe;
package MTier_Menge is new GMenge (TElement => TTier, Element_anzeigen => Attrappe);

Diese Unannehmlichkeit wird vermieden, wenn die Mengenschablone nur mit TElement parametrisiert und Alles_anzeigen nicht als Prozedur sondern als Prozedurschablone exportiert wird. Diese muß dann nötigenfalls gesondert ausgeprägt werden:

package MTier_Menge is new GMenge (TElement => TTier); -- eine andere GMenge, nicht aus (7.8)
procedure Alle_Tiere_malen is new MTier_Menge.Alles_anzeigen (Element_anzeigen => Tier_malen);

Alles_anzeigen läuft (iteriert) über alle Elemente des Multibehälters und führt mit ihnen eine bestimmte Aktion (hier Element_anzeigen) aus. Solche Exportprozedur(schablon)en heißen Iteratoren. Es ist möglich, anstelle eines zweckgebundenen Iterators wie Alles_anzeigen dem Benutzer eine allgemeine Iteratorschablone zur Verfügung zu stellen. Wenn ihre Parameter nicht in, sondern in out vereinbart werden, dann kann sie die Elemente nicht nur lesen, sondern auch beschrieben werden. Der Benutzer kann so viele Iteratoren ausprägen, wie viele er braucht. Die allgemeinere Schnittstelle von GMenge ist also:

generic
	type TElement is private;
package GMenge is
	... -- wie zuvor, aber anstelle der letzten Operation Alles_anzeigen:
	generic
		with procedure Rueckruf (Element: in out TElement); -- führt eine Aktion über Element durch
	procedure Iterator (Menge: in out TMenge); -- ruft Rueckruf für alle eingetragenen Elemente auf
private ...
end GMenge;

Im Programm (7.8) wurde der exportierte abstrakte Datentyp TMenge mit limited private gekennzeichnet; deswegen steht die Zuweisung := nicht zur Verfügung und konnte die Gleichheit "=" neu definiert werden. Die Kennzeichnung des formalen Ausprägungstyps mit private bedeutet, daß ein beliebiger Datentyp als aktueller Ausprägungstyp geeignet ist, außer einem limited private. Dies bedeutet, daß eine Menge aus Eimern zu bilden möglich ist, eine Menge aus Mengen jedoch nicht:

package MEimer_Menge is new GMenge (TElement => MEimer.TEimer); -- zulässig
package MMengen_Menge is new GMenge (TElement => MEimer_Menge.TMenge); -- nicht zulässig

© Prof. Dr. Andreas Solymosi

Rückmeldungen bitte an den Autor solymosi@tfh-berlin.de

Leitseite des Autors