Kurs: Programmieren in Pascal, Teil 5

Mit Pure Pascal dürfen Pascal-Programmierer auf dem Atari erstmals die Tür zur objektorientierten Programmierung aufstoßen. Heben grundlegenden Techniken von OOP widmen wir uns der GEM-Programmierung.

Es kommt wieder Bewegung in die Atari-Programmiererszene. Wurde jahrelang mit bewährten Werkzeugen wie Turbo/Pure C oder GfA-Basic entwickelt, macht nun ein Zauberwort die Runde: OOP, die objektorientierte Programmierung. Sie erfordert ein komplettes Umdenken des Programmierers. Bislang arbeiteten wir mit einer strikten Trennung von Daten und Prozeduren: Die im Deklarationsteil vereinbarten Variablen werden hier durch getrennte Prozeduren angesprochen, arbeiten quasi »ferngesteuert«.

Da aber jede Variable typische Operationen ausführen darf, ergibt sich die Frage, weshalb man nicht eine Einheit aus den Daten und den zugehörigen Prozeduren bildet. Genau diesen Schritt vollzieht die objektorientierte Programmierung: Wir bilden eine Struktur, die sowohl die Daten als auch die Prozeduren zu deren Bearbeitung enthält. Diese Elemente ordnen wir in einer RECORD-ähnlichen Struktur an, die durch die Schlüsselworte OBJECT und END eingegrenzt ist. Zunächst bilden wir eine »Form« des Objekts, indem wir das Objekt als neuen Typ deklarieren, dessen Typnamen wir am besten mit dem Buchstaben »T« beginnen lassen:

TYPE TObjekt = OBJECT
	... (* Variablen *)
	... (*Methoden *)
END;

Die »Füllung« unseres Objektes erledigen wir mit den obengenannten Mitteln: Zunächst geben wir die Variablen nach normalem Muster, jedoch ohne das Schlüsselwort VAR an. Darauf folgen die Deklarationen der Funktionen und Prozeduren, die auf die Variable zugreifen. Sie bezeichnen wir als »Methoden«. Hier die Typdeklaration des einfachen Objektes aus dem Programmbeispiel »DEMO5A.PAS«:

TYPE TPerson = OBJECT
	Name:STRING;
	PROCEDURE SetzName (NeuName: STRING)
	FUNCTION NamensInfo: STRING;
	PROCEDURE Druckdaten;
END;

In diesem Objekt legen wir Personendaten ab, die allerdings nur den Namen umfassen. Die drei Methoden erledigen grundlegende Aufgaben: Setzen und Abfragen des Namens, Ausgabe der Personaldaten. Mit Hilfe dieser Prozeduren ist ein indirekter Zugriff auf die Daten möglich. Ihr Inhalt ist von außen zugänglich, ohne direkt auf die Variable zuzugreifen. Natürlich müssen wir auch die Methoden des Objekts implementieren, was wir im Programm nach dem Deklarationsteil und in einer Unit im IMPLEMENTATION-Teil erledigen. Die Implementation verläuft wie bei jeder anderen Prozedur, im Funktionskopf geben wir jedoch einen »qualifizierten Bezeichnen( in der Form OBJEKTNAME.METHODENNAME an, den wir schon von unserer Arbeit mit RECORDs kennen:

PROCEDURE TPerson.SetzName(NeuName:STRING);
BEGIN
		Name: =NeuName
END;
FUNCTION TPerson.NamensInfo : STRING;
BEGIN
	NamensInfo := Name
END;
PROCEDURE TPerson.Druckdaten;
BEGIN
	WRITELN('Name: ', Name);
END;

Innerhalb dieser Methoden können wir auf die anderen Methoden und Variablen des Objektes über ihren Kurzbezeichner zugreifen. Nun haben wir uns ein erstes Objekt deklariert, doch wie können wir mit ihm arbeiten? Genau wie mit jedem anderen Typ: Wir deklarieren einfach eine Variable vom Typ des Objektes, eine sogenannte »Instanz« des Objektes:

VAR	Person : TPerson; (* Eine Instanz *)
	Person2: TPerson; (* und noch eine *)

Selbstverständlich können wir auch Felder oder Records aus Objekten erzeugen. Ihrer Phantasie sind da keine Grenzen gesetzt. Pure Pascal erzeugt den Programmcode zu den Methoden des Objektes auch bei vielfachem Auftreten sinnvollerweise nur einmal. Auf die Elemente des Objektes greifen wir nun über einen qualifizierten Bezeichner zu, also über den Variablennamen, gefolgt von einem Dezimalpunkt und dem Elementnamen. Programmtechnisch eleganter als diese direkte Zuweisung wäre jedoch der Einsatz der »SetzName«-Methode unseres Objektes, die wir auch über den qualifizierten Namen aufrufen:

Person.SetzName(‚Tamara');

Möchten wir den Namen erfragen, so verwenden wir die »NamensInfo«-Methode:

WRITELN ('Name: ', Person.NamensInfo);
WITH Person DO Druckdaten; (*Auch möglich*)

Genau wie Records können wir Instanzen der Objekte auch einander zuweisen, ein Vergleich miteinander ist nur mit einzelnen Datenfeldern möglich. Ebenso können wir Objekte auch als Prozedurparameter einsetzen:

Person2 := Person;
(* IF Person2 = Person1 THEN ... -> illegal! *)
IF Person2.NamensInfo = Person.NamensInfo
THEN WRITELN('Gleich');
PROCEDURE Test (TestPerson:TPerson)
END;

In unserem Beispiel haben wir bereits mit Datenkapselung gearbeitet, die jedoch leicht umgangen werden kann, da ein direkter Zugriff auf alle Variablen und Methoden von außen möglich ist. Wir können jedoch Teile unseres Objektes gegen Zugriffe von außen schützen: Nur die Methoden des Objektes können auf diese Elemente zugreifen. Hierzu erweitern wir unseren Objektprototypen:

TYPE TObjekt = OBJECT
	... (* Variablen *)
	... (*Methoden
PRIVATE
	... (* Geschützte Variablen *)
	... (* Geschützte Methoden *)
END;

Zunächst führen wir unsere nach außen zugänglichen Variablen und Methoden auf, hängen das Schlüsselwort PRIVATE und die zu verbergenden Variablen und Methoden an. Dieser Schutz funktioniert leider nur, wenn wir die Objektdeklaration in einem Unit durchführen. Der Schutz besteht dann gegen Zugriffe von außerhalb der Unit, innerhalb besteht kein Schutz. Betrachten Sie hierzu die Unit »DEMO5BUN.PAS« und das zugehörige Testprogramm »DEM056.PAS«. Objekte lassen sich natürlich auch sinnvoller einsetzen: In einer Datenbank können wir beispielsweise für jeden Eintrag eine Instanz des Objektes verwenden.

Bei professionellen Programmen ist eine für statische Variablen erforderliche, maximale Datensatzzahl nicht praktikabel. Dynamisches Verwalten von Objekten ist angesagt- Grundsätzlich gehen wir wie bei »normalen« Variablen vor: Wir definieren einen Zeiger, erzeugen mit der NEW-Prozedur ein neues Element und entfernen dies später mit der DISPOSE-Prozedur:

VAR Person :^TPerson; (* Zeiger auf Element *)
...
NEW(Person);
Person^.SetzName ('Oliver');
...
DISPOSE(Person);

Oft müssen wir direkt nach dem dynamischen Erzeugen oder kurz vor dem Verwerfen eines Objektes einige Initialisierungen wie das Setzen von Grundeinstellungen durchführen. Hierzu können wir zwei Prozeduren im Objekt deklarieren: »CONSTRUCTOR« und »DESTRUCTOR« (»DEM05C.PAS«). Als Konvention sollten wir den Konstruktor stets »Init« und den Destruktor stets »Done« nennen, die Parameterzahl passen wir unseren Bedürfnissen an. Der so deklarierte Konstruktor und Destruktor läßt sich wie jede Prozedur einsetzen:

NEW(Person);
Person^.Init ('Normen');
...
Person^.Done;
DISPOSE(Person);

Schön, objektorientierte Programmierung scheint sehr elegant, doch der wahre Nutzen der Sache ist noch nicht erkennbar. Dieser ruht nämlich im Prinzip der Vererbung: Wir dürfen bestehende Objekte erweitern, wobei das neue Objekt die Eigenschaften, sprich Variablen und Methoden, des Vorfahren erbt. Dazu setzen wir hinter das Schlüsselwort OBJECT in runde Klammern den Namen des Vorfahren. Wollen wir das Objekt »TPerson« um einen Gehaltsvermerk erweitern, so ergibt sich folgende Deklaration (»DEMO5D.PAS«):

TMitarbeiter = OBJECT(TPerson) (*Vererbung*)
	Gehalt:REAL;
	CONSTRUCTOR Init (NeuName: STRING; NeuGehalt: REAL);
	FUNCTION GehaltsInfo:REAL;
	FUNCTION Jahresgehalt: REAL;
	PROCEDURE Druckdaten;
END;

Neu hinzugekommen ist die Gehaltsvariable »Gehalt«, die bisherigen Variablen und Methoden werden übernommen. Nur die zusätzlich angegebenen Methoden müssen wir neu implementieren. Der Konstruktor »Init« wurde bereits in »TPerson« deklariert, ist hier aber erneut mit zusätzlichen Parametern aufgeführt. Wir ersetzen hiermit die übernommene »Init«-Methode für das neue Objekt, im Fachjargon als »Überladen« bezeichnet. In der Neuimplementierung dieses Konstruktors können wir aber die alte Implementierung durch einen qualifizierten Aufruf verwerten:

CONSTRUCTOR
TMitarbeiter.Init (NeuName: STRING; NeuGehalt:REAL);
BEGIN
	TPerson.Init(NeuName);
	(* Überladene Prozedur auf rufen
	Gehalt := NeuGehalt
END;

Auf diesem Wege können wir bestehende Objekte wie »Druckdaten« nach unserem Bedarf anpassen, um zusätzlich die Gehaltsdaten auszugeben:

PROCEDURE TMitarbeiter.Druckdaten,
BEGIN
	TPerson.Druckdaten;
	(* Ausgabe der Personendaten (Name) *)
	WRITELN ( 'Gehalt: DM ', Gehalt: 5: 0, 'Im Jahr: ', Jahresgehalt:6:0)
END;

Übrigens können wir auch die Instanzen eines Nachfahren dem Vorfahren zuweisen. Der umgekehrte Fall ist nicht erlaubt. Gleiches gilt für Prozedurparameter und Funktionsergebnisse:

VAR Person : TPerson;
	Mitarbeiter:TMitarbeiter;
	...
	Person := Mitarbeiter;

Betrachten wir ein weiteres typisches Beispiel zur objektorientierten Programmierung, in dem wir Objekte für zwei- und dreidimensionale Vektoren entwerfen, die eine Funktion zur Berechnung ihrer Länge (Betrag) besitzen. Hierzu deklarieren wir uns zunächst ein sogenanntes »abstraktes« Objekt, aus dem wir zwei Nachfahren für zwei- und dreidimensionale Objekte herleiten:

TYPE TVektor = OBJECT
	CONSTRUCTOR Init;
	FUNCTION Betrag: REAL;
END;
TVektor2D = OBJECT(TVektor)
vx,vy:REAL;
CONSTRUCTOR Init(x,y:REAL);
	FUNCTION Betrag:REAL;
END;
TVektor3D = OBJECT(TVektor)
vx, vy, vz:REAL;
CONSTRUCTOR Init (x, y, z: REAL)
	FUNCTION Betrag: REAL;
END;

Die Methoden des »TVektor«-Objektes sind Dummies ohne Programmcode (siehe »DEM05E.PAS«). Während die »init«-Konstruktoren der anderen Objekte jeweils die Vektorkomponenten setzen, liefert die »Betrag«-Funktion die Vektorlänge zurück. Eine Funktion, die den Kehrwert eines übergebenen Vektors bestimmt, sieht dann wie folgt aus:

FUNCTION Betragskehrwert (VAR Vektor: TVektor):REAL;
BEGIN
	Betragskehrwert := 1/ *Vektor.Betrag
END;

Da wir, wie oben beschrieben, auch die Nachfahren eines Objektes als Parameter übergeben können, wären folgende Zeilen erlaubt:

VAR Zeiger:TVektor3D;
...
WRITELN ('1/Betrag = ', Betragskehrwert (Zeiger):7:2);

Testen wir diese Programmzeilen jedoch aus, indem wir das Programm »DEMO5E.PAS« starten, stellen wir fest, daß das Resultat nicht stimmt. Ursache hierfür ist, daß die Methode »Vektor.Betrag« zum Einsatz kommt, die keinerlei Funktion ausübt. Wie aber können wir erreichen, daß die zum Objekttyp passende Methode aufgerufen wird? Die Lösung hierzu ist das Schlüsselwort »VIRTUAL«. Wir setzen diese Zauberformel einfach hinter die Deklarationen der drei »Betrag«-Methoden. Starten Sie bitte das Programm »DEM05FOK.PAS«. Das Resultat ist korrekt, die passende »Betrag«-Methode wurde aufgerufen. Mit dem Schlüsselwort VIRTUAL verwandeln wir eine Methode einer Objekthierarchie in eine virtuelle Methode, machen sie »polymorph«. Bei dem Aufruf der »Betrag«-Methode wird der Prozedur zusätzlich eine Liste der virtuellen Methoden übergeben. Je nach Objekttyp des Parameters pickt Pure Pascal nun die passende Methode heraus.

Es gibt jedoch zwei Bedingungen, die bei virtuellen Methoden erfüllt sein müssen: Eine Methode muß in der gesamten Hierarchie entweder virtuell oder nicht virtuell sein, weshalb es nicht erlaubt wäre, »TVektor2D.Betrag« virtuell und »TVektor3D.Betrag« nicht virtuell zu deklarieren. Außerdem müssen bei virtuellen Methoden stets die Parameterlisten und Funktionsergebnisse übereinstimmen (»DEM05F.PAS«).

Während die Programmierung von Dateioperationen auch ohne objektorientierte Programmierung zu handhaben ist, bereitet eine GEM-Programmierung schon mehr Kopfschmerzen. Nicht ohne Grund mußten Atari-Anwender lange Zeit auf konsequente GEM-Oberflächen warten. Die Unit »DEM05GUN.PAS« zeigt, wie wir die Anwendung der einfachen Datei- und Alarmdialoge vereinfachen:

TYPE PFileSelector = ^TFileSelector;
TFileSelector = OBJECT
	CONSTRUCTOR Init(NewPath:STRING);
	FUNCTION SelectFile(VAR Pathname:STRING) : BOOLEAN;
	PRIVATE
		Path:STRING;
		Name:STRING;
END;

Die Variable »Path« speichert den aktuellen Suchpfad des Dateiauswahlobjektes und wird durch den Konstruktor auf einen Anfangspfad gesetzt, der Dateiname wird gelöscht. Mit der SelectFile-Methode führen wir den Dialog aus, worauf wir bei einem Klick auf »OK« das Funktionsergebnis TRUE und den kompletten Dateipfadnamen in »Pathname« zurückerhalten:

VAR Auswahlfeld:TFileselector;
Pfadname:STRING;
...
Auswahlfeld.Init ('C:\*.DOC')
IF Auswahlfeld. Selectfile (Pfad) THEN ...

Benötigen wir verschiedene Auswahlfelder (Laden, Speichern etc.), so deklarieren wir entsprechend viele Instanzen des Objektes. Die zuletzt aktiven Suchpfade bleiben hierbei bis zur nächsten Ausführung des Dialogs erhalten.

Auch die Anwendung von Alarmfeldern läßt sich weiter vereinfachen:

TYPE PAlertbox=^TAlertbox;
TAlertbox = OBJECT
	CONSTRUCTOR Init (NDefault, Icon: INTEGER; Text, Buttons:STRING);
	FUNCTION Alert:INTEGER;
	PRIVATE
		Default:INTEGER;
		Textinfo:STRING[160];
END;

Dem Konstruktor übergeben wir die Nummer des Defaultknopfes (0-3), die Art des Piktogrammes (Konstanten »AlertNone«, »AlertNote«, »AlertWait« und »AlertStop«), den Informationstext (Zeilen durch »|« getrennt) und die Knopftexte (durch »|« getrennt). Mit dem Aufruf der »Alert«-Methode stellen wir die Alertbox dar, als Ergebnis erhalten wir die Nummer des gewählten Knopfes:

VAR Alarm:PAlertbox;
...
Alarm.Init (1, AlertWait, " Programm | beenden...?", "Ja | Nein") ;
WHILE Alarm.Alert <> 1 DO;
...

Das Programm »DEMO5G.PAS« demonstriert die Arbeit mit der Unit. Objektorientierte Programmierung eröffnet aber noch weitergehende Perspektiven. So arbeitet die Firma Softdesign an einer objektorientierten Klassenbibliothek namens »ObjectGEM«, die offensichtlich Parallelen zu »ObjectWindows« (Bibliothek von Turbo-Pascal für Windows) zeigt. Eine solche Library könnte beispielsweise den kompletten Rahmen für eine GEM-Applikation in Form eines Applikations-Objekts bieten. Für Fenster würde beispielsweise ein Fenster-Objekt existieren, das Routine-Arbeiten wie das Verschieben oder das Neuzeichnen des Fensters erledigt.

Zum Abschluß unserer »Tour de Pascal« wünsche ich Ihnen eine erfolgreiche Programmierzukunft mit Pure Pascal. (ah)


Frank Mathy
Aus: TOS 01 / 1993, Seite 86

Links

Copyright-Bestimmungen: siehe Über diese Seite