Dieses Kapitel gehoert zum Kurs Programmieren in Oberon des Fachbereichs Informatik.

Typen als Objekte

Bearbeiten

Heutzutage kann man kaum über eine moderne Programmiersprache reden, ohne das Thema Objektorientierung anzusprechen. Da eben Oberon so eine moderne Sprache ist - die Version 2 wurde immerhin 1991 veröffentlicht - muss ich wohl auch darüber zu sprechen kommen.

Was sind denn diese Objekt-Dinger?

Bearbeiten

Schaut man sich ein wenig in der Computer-Literatur um, wird man schnell erkennen das das Lieblingsbeispiel fast aller Autoren für die Beschreibung von Objekten geometrische Figuren sind. Diese Bücher wurden alle von Professoren und anderen äußerst intelligenten Menschen geschrieben, und da ich mich noch nicht dazuzählen will, will ich es wagen, mit dieser Beispiel-Konvention zu brechen.

Also nun, genug gelabert: Objekte sind wie Hunde - naja, irgendwie. Springen wir doch sofort mit unserem jetzigen Wissen ins kalte Wasser und deklarieren wir doch einen Hund in Oberon:

TYPE
	Hund = RECORD
		name : ARRAY 20 OF CHAR;
	END;

Mehr brauch der Computer über unseren Hund nicht zu wissen. Jetzt, jeder von euch, der/die schon mal mehr als einen Hund gesehen hat (mögen es bitte viele sein), weiss, dass es sowas wie verschiedene Marken (naja, Rassen?) gibt, welche das Verhalten des Hundes zu einem großen Teil beeinflussen (wollen wir einfach mal annehmen).

Wie wollen wir dieses Wissen nun in unseren Typus Hund packen? Nun, ein Ansatz wäre vielleicht:

CONST
	hUnbekannt = 0;
	hChihuahua = 1;
	hBoxer = 2;
	hWindhund = 3;

TYPE
	Hund = RECORD
		name : ARRAY 20 OF CHAR;
		marke : INTEGER;
	END;

wobei die Variable marke jeweils auf den gewünschten Wert gesetzt werde. Eine Prozedur, welche das Verhalten eines Hundes wiedergeben muss, kann dann wie folgt aussehen:

PROCEDURE EinVerhalten ( h : HUND );
	BEGIN
		IF h.marke = hUnbekannt THEN
			...
		ELSIF h.marke = hChihuahua THEN
			...
		ELSIF h.marke = hBoxer THEN
			...
		ELSIF h.marke = hWindhund THEN
			...
		END;
	END EinVerhalten;

Ist doch etwas mühsam, oder? Jedesmal wenn wir nachher noch einen Hund in unsere Sammlung aufnehmen, müssen wir jede Verhaltensprozedur erweitern. Viel leichter wäre doch, wenn wir dem Hund direkt sagen könnten, was er machen sollte. Hört sich abstrakt an? Isses auch!

Die variable Prozedur

Bearbeiten

Um das ganze irgendwie veranschaulichen zu können, was mit modernen Sprachen wie Oberon2 noch schwierig ist, werde ich mich Oberon1 bedienen, und erst nachher erklären wie es in Oberon2 gemacht wird. Da die Objektorientierung bei Oberon1 eher wie ein voreiliger "Patch" aussieht, um in der Werbung dann sagen zu können, es sei auch Objektorientiert, eignet es sich prima als Veranschaulichung, da es all die grausigen Details, welche der Compiler eigentlich selber übernehmen sollte, den Programmierer ueberlaesst.

Und nun zurück zum Thema: so, wie wir bei der Typendeklaration jedem RECORD eine Variable unterschieben konnten, können wir dies jetzt auch mit Prozeduren tun. Als Beispiel erweitern wir doch unseren Hund insofern, dass wir ihm das Bellen beibringen.

TYPE
	Hund = RECORD
		name : ARRAY 20 OF CHAR;
		bellen : PROCEDURE ( h : Hund );
	END;

Unser Hund enthält jetzt eine Variable namens bellen, welche eine Prozedur mit dem Parameter h : Hund enthält. Eigentlich "enthält" diese Variable nicht die Prozedur, sie zeigt nur darauf. Will man nun einen Hund zum bellen bringen, muss man folgendes unternehmen (natürlich nur in Oberon, mit richtigen Hunden muss man anders umgehen):

PROCEDURE Bellen ( h : Hund );
	BEGIN
		Out.String(h.name); Out.String(": ");
		Out.String("Waf!"); Out.Ln;
	END Bellen;

PROCEDURE neuerHund ( VAR h : Hund ; n : ARRAY OF CHAR );
	BEGIN
		h.name := n;
		h.bellen := Bellen;
	END NeuerHund;

PROCEDURE Go*;
	VAR
		h : Hund;
	BEGIN
		NeuerHund(h,"Bingo");
		h.bellen(h);
	END Go;

Zuerst deklarieren wir eine Prozedur Bellen, welche das eigentliche Bellen übernimmt, dann schreiben wir die Prozedur NeuerHund, welche den Hund mit namen initialisiert und den wert der Variable bellen setzt. Zuletzt deklarieren wir einen Hund, nennen ihn Bingo und lassen ihn bellen:

Bingo: Waf!

Toll, nicht? Fast wie ein echter.

Wie lösen wir aber das Problem mit den Marken? Ein Chihuahua bellt sicher nicht wie ein Boxer, und ein Windhund ist zu blöd um zu bellen, also wie geht man vor?

PROCEDURE BoxerBellen ( h : Hund );
 BEGIN
		Out.String(h.name); Out.String(": ");
		Out.String("ARF!"); Out.Ln;
	END BoxerBellen;

PROCEDURE ChihuahuaBellen ( h : Hund );
 BEGIN
		Out.String(h.name); Out.String(": ");
		Out.String("yap!"); Out.Ln;
	END ChihuahuaBellen;

PROCEDURE NeuerBoxer ( VAR h : Hund ; n : ARRAY OF CHAR );
	BEGIN
		h.name := n;
		h.bellen := BoxerBellen;
	END NeuerBoxer;

PROCEDURE NeuerChihuahua ( VAR h : Hund ; n : ARRAY OF CHAR );
	BEGIN
		h.name := n;
		h.bellen := ChihuahuaBellen;
	END NeuerChihuahua;

PROCEDURE Go*;
	VAR
		boxer, chihuahua : Hund;
	BEGIN
		NeuerBoxer(boxer,"Max");
		NeuerChihuahua(chihuahua,"Moritz");
		boxer.bellen(boxer);
		chihuahua.bellen(chihuahua);
	END Go;

So, Problem gelöst. Lässt man jetzt Go fahren, kriegt man sowas:

Max: ARF!
Moritz: yap!

Obwohl beide Hunde, boxer und chihuahua vom gleichen Typ sind, werden sie mit verschiedene Prozeduren initialisiert, und bekommen verschiedene Prozeduren fürs bellen. Sagt man dann einem der Hunde, er solle bellen, tut er dies mit seiner eigenen zugewiesenen Prozedur.

Da die Prozedur bellen wie eine normale Variable behandelt wird, kann sie auch weitervererbt werden und von zukünftigen Generationen auch überschrieben, sprich neu gesetzt, werden. Wichtig ist einfach, dass die Variable bellen initialisiert wird, bevor sie verwendet wird. Zudem darf sie nur Prozeduren zugewiesen bekommen, welche die gleiche Parameterliste wie die Variable in der RECORD-Deklaration besitzen.

Back to the Future

Bearbeiten

So, jetzt wo wir gesehen haben, wie sich das Objektverhalten in Oberon1 simulieren lässt, können wir verstehen wie Objekte funktionieren. Dies lässt sich jedoch einiges einfacher machen:

TYPE
	Hund = RECORD
		name : ARRAY 20 OF CHAR;
	END;
	hBoxer = RECORD ( Hund )
	END;
	hChihuahua = RECORD ( Hund )
	END;
	hWindhund = RECORD ( Hund )
	END;

Anstatt einen Typus für Hund zu definieren und die Markeneinteilung durch verschieden Initialisierungen vorzunehmen, deklarieren wir eine sogenannte "Oberklasse" für Hund, welche den namen, jedoch keine Prozedurvariable fürs bellen enthält. Später deklarieren wir noch "Unterklassen" von Hund, welche die verschiedenen Marken repräsentieren sollen.

Wie gesagt, haben wir keine Prozedurvariable deklariert. Dies müssen wir auch nicht tun, denn Oberon2 übernimmt diese kleine Unannehmlichkeit, sobald wir eine typengebundene Prozedur für Hund schreiben.

PROCEDURE ( h : Hund ) Bellen;
	BEGIN
		Out.String(h.name); Out.String(": ");
		Out.String("Waf!"); Out.Ln;
	END Bellen;

Der/Die aufmerksame LeserIn wird bemerkt haben, dass die Parameteruebergabe plötzlich hinter dem Prozedurnamen steht. Bravo. In Oberon2 können wir nämlich so eine Prozedur an einen Typus binden. Diese spezielle Prozedur wird dann wie folgt aufgerufen:

PROCEDURE Go*;
	VAR
		h : Hund;
	BEGIN
		h.name := "Bingo";
		h.Bellen;
	END Go;

In diesem Beispiel muss die Variable h vom Typus Hund sein oder von einer Unterklasse von Hund. Auch zulässig wäre also auch:

PROCEDURE Go*;
	VAR
		b : hBoxer;
	BEGIN
		b.name := "Alvarez";
		b.Bellen;
	END Go;

da hBoxer eine Unterklasse von Hund ist, also all die gleichen Variablen enthält und sich deswegen sich ebenfalls wie ein Hund benehmen kann.

Weiter oben haben wir jedoch gesehen, dass verschiedene Marken verschieden bellen. Wir müssten also z.B. für den Typus hBoxer eine neue bell-Prozedur zuweisen können. Dies geht wie folgt ab:

PROCEDURE ( b : hBoxer ) Bellen;
	BEGIN
		Out.String(b.name); Out.String(": ");
		Out.String("ARF!"); Out.Ln;
	END bellen;

Obwohl die Prozedur den gleichen Namen hat wie die für nomale Hunde, schluckt sie der Compiler dennoch, denn sie ist an einen Typus gebunden, welcher eine Unterklasse von Hund darstellt.

Es kommt aber noch mehr dazu: will ein hBoxer so bellen wie seine Oberklasse, kann er folgendes probieren:

b.Bellen^;

Dies kann ziemlich nützlich sein, zwar nicht für Hunde, aber man stelle sich vor, wir haben eine Oberklasse Mutter und eine Unterklasse Kind. Wir deklarieren eine Initialisierungsprozedur für die Mutter. Das Kind muss auch dasselbe initialisieren wie die Mutter, und noch einiges mehr. Um den ganzen Kram nicht zweimal schreiben zu müssen, kann man die Initialisierungsprozedur des Kindes noch die der Mutter aufrufen lassen. Das ganze koennte dann zum Beispiel so aussehen:

TYPE
	Mutter = RECORD
		einWert : INTEGER;
	END;
	Kind = RECORD ( Mutter )
		zweiWert : INTEGER;
	END;

PROCEDURE ( m : Mutter ) Init;
	BEGIN
		einWert := 1234;
	END init;

PROCEDURE ( k : Kind ) Init;
	BEGIN
		k.Init^;
		zweiWert := 5678;
	END init;

Die Prozedur k.init ruft dann mittels k.Init^ die Initialisierungsprozedur der Oberklasse auf, die erledigt ihr Zeugs, und k.Init muss dann nur noch das in seiner Typendeklaration neu dazugekommene Zeugs erledigen.

Im Beispiel scheint dies ein wenig sinnlos - wir können doch die Anweisung in m.Init direkt übernehmen und ersparen uns somit einiges an Arbeit. Nützlich ist das Gaze erst, wenn die Oberklasse etwas komplizierteres ist, so etwa ein Fenster auf einer Benutzeroberfläche. Da wir zum Teil gar nicht wissen, was die Initialisierungsprozedur der Oberklasse anstellt, versuchen wir erst gar nicht, sie zu replizieren, sondern rufen sie direkt auf.

"Cry 'havoc!' and let slip the dogs of war"[1]

Bearbeiten

Nun, da wir unsere bellenden Hunde haben, lassen wir sie mal in einem Chor singen. Wir bauen dazu eine liste von Hunde, gehen diese dann Hund für Hund durch und fordern jeden auf zu bellen. Den Typus Hund erweitern wir noch zwecks der Liste um ein Element next.

TYPE
	pHund = POINTER TO dHund;
	dHund = RECORD
		name : ARRAY 20 OF CHAR;
		next : pHund;
	END;
	pBoxer = POINTER TO dBoxer;
	dBoxer = RECORD ( dHund )
	END;
	pChihuahua = POINTER TO dChihuahua;
	dChihuahua = RECORD ( dHund )
	END;
	pWindhund = POINTER TO dWindhund;
	dWindhund = RECORD ( dHund )
	END;

Nun, wir haben jetzt unsere verschiedenen Marken deklariert, es fehlen nur die Prozeduren:

PROCEDURE ( h : pHund ) Bellen;
	BEGIN
		Out.String(h^.name); Out.String(": ");
		Out.String("Waf!"); Out.Ln;
	END Bellen;

PROCEDURE ( b : pBoxer ) Bellen;
	BEGIN
		Out.String(b^.name); Out.String(": ");
		Out.String("ARF!"); Out.Ln;
	END Bellen;

PROCEDURE ( c : pChihuahua ) Bellen;
	BEGIN
		Out.String(c^.name); Out.String(": ");
		Out.String("yap!"); Out.Ln;
	END Bellen;

PROCEDURE ( a : pWindhund ) Bellen;
	BEGIN
		Out.String(a^.name); Out.String(": ");
		Out.String("..."); Out.Ln;
	END Bellen;

Ihr werdet wohl zwei Sachen gemerkt haben. Erstens: wir können die Prozeduren auch an Zeiger auf Typen binden, nicht nur an die Typen selbst. Zweitens: der Windhunde macht nix. Ist auch richtig so. Windhunde tun eh nix, die sind zu blöd dazu.

Nun müssen wir, um ein fertiges Programm zu haben, noch zwei Prozeduren hinzufügen.

PROCEDURE Singen ( h : pHund );
	BEGIN
		WHILE h # NIL DO
			h.Bellen;
			h := h.next;
		END;
	END Singen;

PROCEDURE Go*;
	VAR
		h : pHund;
		b : pBoxer;
		c : pChihuahua;
		a : pWindhund;
	BEGIN
		NEW(h); NEW(b); NEW(c); NEW(a);
		h.name := "Bingo"; b.name := "Alvarez";
		c.name := "Max"; a.name := "Moritz";
		h.next := b;
		b.next := c;
		c.next := a;
		a.next := NIL;
		Singen(h);
	END Go;

Interessant ist eigentlich nur die Prozedur Singen, denn sie kriegt einen Zeiger auf einen Hund und geht die ganze Liste durch, als wären es alle nur Hunde. Das tolle daran ist, dass sich Oberon2 merkt, was jedes Objekt für einen Typus hat, und dann die entsprechende Prozedur aufruft.

Nehmen wir den zweiten Hund in der Liste als Beispiel. Er ist ein Boxer, aber auch ein Hund. Die Zuweisung

h.next := b;

ist zulässig, da die Variable next ein Zeiger auf einen Hund sein sollte und b ein Zeiger auf eine Unterklasse von Hund ist. Als die Hunde zu singen anfangen, geht der erste durch wie nix, denn er ist nur ein Hund. Der zweite ist jedoch ein Boxer. Hier muss man sich wieder Oberon1 ins Gedächtnis rufen: der Typus Hund enthält eine Variable, welche auf die bell-Prozedur zeigt. Da Boxer eine Unterklasse von Hund ist, enthält sie diese Variable auch. Die Prozedur Singen ruft einfach die Prozedur auf, welche an dieser Variablen hängt. Beim Boxer ist es halt eine, welche ARF! sagt. Die Zuweisung dieser Prozeduren hat Oberon2, im Gegensatz zu Oberon1, für uns automatisch uebernommen.

Ist alles gut gelaufen, kriegen wir am Schluss sowas:

Bingo: Waf!
Alvarez: ARF!
Max: yap!
Moritz: ...

Windhunde sind eben blöd...

Fussnoten

Bearbeiten
  1. Aus Hamlet von Shakespeare.