Session D-WOOD

Praktische OO-Programmierung

Jürgen Wondzinski
ProLib Software GmbH

 


Haben Sie jemals die fünf Ausgaben der vierbändigen Trilogie „Per Anhalter duch die Galaxis" von Douglas Adams gelesen? Na, dann wissen Sie schon ungefähr, was Sie auf den folgenden Seiten erwartet… (Wenn Sie dieses Meisterstück der absurden Science-Fiction wirklich noch nicht kennen, dann sollten Sie es aber schnellstens nachholen!)

Aber genau so wie dem Helden jener Geschichte geht es wohl den meisten FoxProProgrammierern (und sicher auch anderen) heute:

Jahrelang war man in seiner vertrauten Umgebung zuhause, die Programme gingen ihren gewohnten Gang, und die (DOS-) Welt war im Großen und Ganzen in Ordnung. Man hatte seinen eigenen Stil entwickelt, der Programmfluß war wohlgeordnet vom Start übers Menü, durch die einzelnen Unterprogramme hindurch, bis hin zur Abschlußroutine. Doch dann kam eines Tages eine gewaltige Macht, und ihr Herrscher (zwar noch jung, dafür aber nervös an der Brille rückend) verkündete, daß diese vertraute Welt nichts mehr tauge, dem Fortschritt im Wege stehe und daher vernichtet wird. Tja, und da stehen Sie nun, auf dem Sprung in die weite Welt voller neuer Möglichkeiten und Gefahren. Ihre Programme sollen nun Objekte sein, die losgelöst jeder Fessel durch die bunte Windows-Welt schwimmen. Vorbei die Zeit der klaren Abläufe; der Anwender bestimmt nun, und ihr Programm hat nur noch zu reagieren.

Im Folgenden will ich Ihnen helfen, diesen Wandel so einfach wie möglich durchzumachen. Sicher ist Ihnen manches, was Sie hier lesen werden, schon bekannt, denn FoxPro hat ja schon seit der Version 2.0 (anno 1991) viele Ideen der objektorientierten Programmierung ansatzweise verwirklicht. So gesehen, fehlt uns nicht mehr recht viel: ein paar neue Befehle hier, ein etwas anderer Ablauf da, und fertig ist FoxPro 3 !

 

Wenn Sie heutzutage auf einem ganz normalen Programmierertreffen einigermaßen heil überleben wollen, dann müssen Sie nur ganze zehn Worte beherrschen: Objekte, Methoden, Nachrichten, Klassen, Unterklassen, Instanzen, Vererbung, Kapselung, Abstraktion und für Überflieger: Polymorphismus. Wenn Sie es ganz doll treiben wollen, dann verwenden Sie einfach noch die englischen Begriffe wie: inheritance oder encapsulation. Vielleicht üben Sie das heute abend schon mal beim Duschen, dann haben Sie beim nächsten Treffen genügend Bewunderer…

Bevor wir uns nun in diese „Schöne Neue Welt" stürzen und erforschen, was diese tollen Worte tatsächlich zu bedeuten haben, sollten wir vielleicht noch kurz die jetzige Programmierwelt kritisch betrachten. Schon was von dem Schlagwort der sog. Softwarekrise gehört? Soll heißen: Programme werden nur mit Verspätung fertig, kosten mehr als geplant und haben diverse Fehler.

Zusammengefaßt läßt sich unser Beruf (und unser Problem) sicher so darstellen: Unsere Anwender sammeln immer mehr Daten, die sie immer besser auswerten und verarbeiten wollen. Noch während wir an den dazu notwendigen Programmen schreiben, hat der Kunde schon wieder ganz andere Anforderungen und Wünsche. (Ist ja auch sein gutes Recht, wir wären die Letzten, die ihm diese Wünsche verübeln würden; schließlich leben wir ja davon. Aber muß es denn so schnell sein?) Während wir noch am Testen und Fehlerbeseitigen sind, liegen schon die nächsten Wunschzettel auf dem Tisch. Tatsache ist, daß man mit den althergebrachten Programmiermethoden den sich ständig ändernden Anforderungen der heutigen Zeit nicht mehr Herr wird. Dauert doch das Erstellen eines Pflichtenheftes meist schon länger als mancher Abteilungsleiter dafür verantwortlich ist!

Die Lösung dieses Teufelskreises heißt RAD, oder auch Rapid Application Development. Darunter versteht man ganz allgemein das schnelle Erstellen eines Prototypen, am besten im Beisein des Auftraggebers, denn nur in Zusammenarbeit mit dem Anwender kann man schon in der Planungsphase grundlegende Denkfehler und Probleme umgehen. Aus diesem Prototypen wird wiederum durch Verfeinern und Erweitern das Endprodukt; d.h. der Prototyp ist nicht mehr nur Muster, sondern der Kern des Ganzen.

Dieses Verfahren bedingt fertige Bausteine, die schnell und einfach zu einer Lösung zusammengesetzt werden können. Man braucht erprobte, in sich geschlossene Einheiten, die speziell zur Lösung einer bestimmten Aufgabe entwickelt worden sind. Andererseits sollen aber diese definierten Abläufe jederzeit einfach an spezielle Anforderungen anpaßbar sein. Mit der üblichen „Codiererei" kommen wir hier nicht mehr ans Ziel.

Nehmen wir mal einen bildlichen Vergleich her, damit Ihnen die Einsicht leichter fällt:

Angenommen, Sie wollen ein Haus bauen. Am einfachsten geht das, indem Sie alles selber machen, denn immerhin ist’s ja ihr Haus, und Sie wissen, wie es werden soll. Also lernen Sie mauern, schweißen, malen, schreinern usw. Dabei wird zwar ab und an der Daumen dran glauben müssen, und manche Wand wird etwas krumm sein, aber es ist eben ihr Haus. Unterm Strich haben Sie dafür zwei Jahre benötigt, aber was solls. Und daß bei der Klospülung ausnahmsweise dampfendes Wasser kommt, wird von Ihnen der Einfachheit halber als besonderes „Feature" und nicht als „Bug" bezeichnet… Wesentlich besser wäre es allerdings gewesen, wenn Sie z.B. die Wasserrohre vom Installateur hätten legen lassen, oder die Fenster gleich fertig beim Schreiner bestellt hätten.

Der Stand der Softwareproduktion heute gleicht diesem Vorgehen. Anstatt Teilbereiche mit fertigen Spezialmodulen abzudecken, versuchen wir alles selbst zu machen, auch wenn wir uns erst in Teilbereiche langwierig einarbeiten müssen. Themen wie Serielle Kommunikation, Barcodes, Electronic Banking usw sind sicherlich Gebiete, die einiges an Erfahrung voraussetzen. Unser Beruf wandelt sich immer mehr zum Problemlöser, zum Architekten. Wir bekommen eine Aufgabe und versuchen diese mit soviel Standardbausteinen wie möglich zu realisieren. Dabei interessiert es uns weniger, wie diese Bausteine intern funktionieren, wichtig ist, daß sie fehlerfrei und stabil sind. Diese Grundidee ist der Ursprung der objektorientierten Programmierung (Na endlich, nun wir sind wieder beim Thema…) Im folgenden will ich Ihnen durch eine Gegeüberstellung von prozeduraler zu objektorientierter Programmierung die Unterschiede und Gemeinsamkeiten aufzeigen. Dabei versuche ich möglichst viele Gemeinsamkeiten und Ähnlichkeiten aufzudecken, um so den Umstieg zu erleichtern. (Der echte C++ bzw SmallTalk Freak wird sich mit Grausen abwenden, aber man möge mir verzeihen...)

Wenn man sich eine normale FoxPro Prozedur mal ansieht, dann könnte man z.B. folgendes Konstrukt finden:

Wie man unschwer sieht, eine typische Messagebox. Abgesehen davon, daß wir dies auch über die neue MESSAGEBOX() Funktion in einer Zeile abarbeiten könnten, wollen wir diesen Code nun Schritt für Schritt ins VFP Modell umsetzen. Denn er hat so ziemlich alles drin: Ein Fenster, Textausgabe, eine Eingabe, eine Funktion usw.

Nun wollen wir zur Gegenüberstellung das Ganze mal in OO-Geschreibsel ansehen: Die folgenden Schritte können Sie natürlich auch visuell durchführen (nicht umsonst heißt ja FoxPro auch so), aber es schadet nicht, sich die dahinterliegende Programmiertechnik vor Augen zu führen.

Zuerst also wie immer der Aufruf der Maske:

Danach folgt die Definition:

 

Nachdem Sie dieses Programm überflogen haben, werden Sie sicherlich einiges Vertrautes finden, manches ahnen Sie schon und manches ist sicherlich noch unklar. Gut! Gehen wir mal Schritt für Schritt vor:

Zurückkommend auf das Stichwort RAD, ist das Grundprinzip das Zerlegen eines Problems in viele kleine Einzelkomponenten, deren Ablauf exakt beschrieben wird. Durch das Zerlegen auf viele Einzelkomponenten können Sie in der Summe mit weniger Code auskommen, da sich Grundmodule immer wieder verwenden lassen, und nur noch die Änderungen zum Standard zugefügt werden müssen. Nennen wir diese Grundmodule einfach OBJEKTE.

Für unser neues Programm müssen wir also wohl so ein Objekt definieren; nennen wir es TestOOP. Dummerweise liegen Objekte nicht einfach so rum, sondern sind gut aufgeräumt in sog. Bibliotheken, oder wie der fortschrittliche Programmierer sagt: Libraries. Von diesen haben wir in VFP zwei Sorten: Die Visuelle und die programmatische (Nein, nicht "problematisch"!). Verwenden kann man diese Libraries durch zwei Befehle:

oder

Da wir natürlich alte Codierer (bzw Hacker) sind, bleiben wir erst mal bei der zweiten, der Prozedurdatei. Aber das ist ja keine Änderung gegenüber früher? Ganz recht. Damit´s anders und objektorientiert wird, muß also erst mal ein neuer Befehl her:

Neue Befehle, neues Glück? Was sind denn Klassen?

Eine Klasse ist sozusagen die Theorie, die Beschreibung eines Objektes. Eine Klasse sieht zwar aus wie lauffähiger Code, wird aber NIE ausgeführt. Es ist nur eine Vorgabe für das später einmal erzeugte Objekt. In einer Klasse sind alle Eigenschaften und das Verhalten definiert. Für unsere Meldungsfenster, hoppla: Meldungs-OBJEKT, bedeutet dies, daß hier diverse Einstellungen definiert sind: Größe, Aussehen, Farbe, Position, Texte, dies sind sog.

oder neudeutsch: Eigenschaften. Im Prinzip also sowas wie eine spezielle Art Variable, die aber nur in dieser Klasse existieren. Ein Property wird genauso wie eine Variable zugewiesen. Es kommt darauf an, wo sie eine Variablenzuweisung machen: Innerhalb eines DEFINE CLASS Statements wird’s ein Property, innerhalb einer Methode bzw Prozedur wird’s eine normale Variable. Im Gegensatz zu einer Variable behält aber ein Property seinen Wert über die gesamte Lebensdauer des Objektes. (Es ist also prinzipiell soetwas wie eine STATIC Variable.)

sind Funktionen und Prozeduren, die dieses Objekt ausführen kann. Alles was innerhalb eines DEFINE CLASS Bereiches mit PROCEDURE / FUNCTION beginnt, wird automatisch zur Methode erklärt. Ansonsten bleibt alles beim Alten. Unsere Schaltfläche führt hier die CLICK-Methode aus, wenn man sie betätigt. (Dies entspricht dem VALID der prozeduralen Denke). Durch Voranstellen des Objektnamens weiß FoxPro, welche CLICK Prozedur zu welchem Objekt gehört.

Wie sie sehen, ist das ziemlich vertrauter Code. Eine Klasse ist im weitesten Sinne eigentlich ein Programm, mit Variablenzuweisungen und Funktionen drin.

Wichtig ist auch, daß eine Klasse sich immer von einer Basislasse ableitet (hier "AS form"). In VFP gibt es diverse Basisklassen, eine davon ist die FORM, die nicht anderes macht, als ein Fenster darzustellen. Alle Basisklassen haben eine Schwung fertigdefinierter Methoden und Properties, die das Standardverhalten und Aussehen bestimmen. So hat die Formklasse eine SHOW Methode, aber auch INIT, ACTIVATE usw. Und auch die Properties sind mit mehr oder weniger sinnvollen Werten vorbelegt.

Die DEFINE CLASS Zeile wird irgendwann weiter unten dann mit einem ENDDEFINE abgeschlossen. Danach kann die nächste Klasse definiert werden. Also auch hier wieder eine Analogie zur alten Prozedurmethode. (Übrigens können Sie nun auch Prozeduren und Funktionen sauber mit einem ENDPROC bzw ENDFUNC abschliessen. Bringt zwar nix, sieht aber besser aus...)

Erst durch das Instanzieren wird aus einer Klasse ein Objekt, mit dem man tatsächlich arbeiten kann. Das Objekt übernimmt dabei die theoretische Beschreibung der Klasse.

Durch diesen Befehl wird nun endlich etwas lauffähiges erzeugt. Von einer Klasse können (theoretisch) beliebig viele Objekte erstellt werden (zumindest bis der Hauptspeicher platzt). Dies sind dann die multiplen Instanzen. Man könnt´s auch Mehrfach-Aufruf nennen..

Alle diese Objekte sind erst mal eineiige Zwillinge mit den selben Eigenschaften und Methoden. Durch das Kürzel o am Anfang der Variable oForm1 deutet man an, daß der Inhalt dieser Variable vom Typ OBJEKT ist, es ist eine Referenz auf das erstellte Objekt (Gebüldete Leute sprechen daher auch von einer Objektreferenz). Oder bildlich gesehen: Es ist der Ankerpunkt, an dem sich ein Objekt im großen dunklen Hauptspeicher anklammert. Solange dieser Ankerpunkt existiert, solange existiert auch das dazugehörige Objekt. Durch ein hinterhältiges

können wir dieses Objekt gnadenlos ins Datennirwana absegeln lassen. Interessanterweise kann man auch mehrere Anker (bzw Objektreferenzen) erstellen:

Dadurch existieren zwei Verweise auf ein und dasselbe Objekt, und es müssen beide Verweise gelöscht werden, bevor das Objekt erlischt (Also soetwas wie ein USE AGAIN auf ein Objekt).

Aber was machen wir nun mit unserer schönen Objektreferenz? Für uns stellt sich die Referenz wie ein Variablen-Alias dar, und genauso arbeitet man auch damit. Z.B. kann man nun durch gezieltes Zuweisen von neuen Werten von außen Einfluß auf das Objekt nehmen.

Und schon unterscheiden sich die beiden ehemaligen eineiigen Zwillinge voneinander. Wichtigste Merkregel: Objekte und deren Inhalt sind komplett abgeschottet. Im Fachchinesisch heißt das dann auch

Zwar hat jedes Objekt ein Property LEFT, aber diese überschreiben sich nicht. Nur über den Objektnamen ist an die internen Properties ranzukommen.

Eine Steigerung davon ist die PROTECTED Eigenschaft, mit der man PROPERTIES und METHODEN nach aussen hin unsichtbar machen kann. Wir könnten nun in unsere Klassendefinition gleich am Anfang einfügen:

und unser Fenster würde wie gehabt funktionieren. Nur das Zuweisen eines neuen Wertes auf die Eigenschaft LEFT bringt nun die Fehlermeldung "Eigenschaft LEFT nicht gefunden", obwohl ein Fenster natürlich weiterhin eine linke Position haben muß. Der einzige Zugriff kann nur durch eigene, objektinterne Methoden erfolgen. Man könnte nun also in einer solchen internen Methode die LEFT Property wie gelernt verändern: oForm1.LEFT = 20 und es würde funktionieren.... solange wir die Objektreferenz beim CREATEOBJECT auch tatsächlich oForm1 nennen! Wir haben aber schon ein oForm2 und ein oForm3, was nun? Die Lösung ist ein Platzhalter namens

Über den "Alias" THIS wird immer auf das aktuelle Objekt Bezug genommen, ohne daß man dazu dessen Namen wissen muß (Vergleichbar der Funktion PROGRAM(), die innerhalb eines Programms dessen Namen zurückgibt). Also

und schon ist es generisch, egal wie unser Objekt heißt. Außerhalb eines Objektes gibt es kein THIS! Dies ist wichtig, wenn sie zB über ein ON KEY LABEL oder einen Menüaufruf eine Routine ausführen lassen, während unsere Form am Bildschirm ist. Von außerhalb kann man entweder mit dem echten Objektnamen arbeiten, oder mit einer Property des VFP Hauptfensters, das den Namen des aktuellen Objektes beinhaltet:

ist vom Inhalt her identisch zu oForm1. Wenn wir zuweisen können, können wir auch die Werte abfragen. Im Befehlsfenster eingegeben:

zeigt uns den Wert dieses Properties an. Genauso wie wir die Properties über die Objektreferenz beeinflussen können, genauso können wir auch die Methoden aufrufen:

Dies führt dazu, daß unser bisher unsichtbares Fenster auf dem Bildschirm erscheint. (Was lernen wir daraus? ACTIVATE WINDOW heißt nun <objekt>.SHOW) Unsere Meldung erscheint, alles funktioniert wie gehabt. Sehr schön!

Wie ist denn nun der Meldungstext und die Schaltfläche entstanden? Nun das gute alte @ SAY ist verschwunden, auch Textausgaben sind nun echte Objekte, sog. Labels, die wie jedes andere Objekt anpaßbar sind. Wir haben also nun Objekte in einem Oberobjekt. Die Form ist also ein sog.

Einem Container kann man weiter Objekte hinzufügen. Durch den Befehl ADD OBJECT werden innerhalb der Form weitere Objekte eingelagert:

Durch ihren eindeutigen Namen sind auch diese wieder ansprechbar:

Angenommen, wir wollen nun noch eine Hinweisbox, aber mit einer anderen Farbe. Grau ist zwar edel, aber rot würde die Wichtigkeit doch noch mehr unterstreichen. Nun könnten wir entweder die gesamte Klasse unter einem anderen Namen neu anlegen, oder aber wir sind faul und recyclen den schon vorhandenen Code durch

Dadurch entsteht eine neue Klasse, die sich auf eine andere Klasse bezieht und einige Änderungen vornimmt. Dies könnten z.B. neue oder geänderte Properties und Methoden sein.

Hier wurde also ein zweites Fenster designed, das sich nur in der Farbe vom ersten Fenster unterscheidet. Alles andere bleibt wie das Original. Sie sehen hier ganz deutlich die Code-Einsparung!

Wenn Sie nun eine Instanz von HinweisRot erzeugen, ist diese bis auf die Farbe identisch zu Hinweis. Tolle Sache... Aber es kommt noch besser! Wenn wir z.B. die Größe von HINWEIS in der Klassendefinition ändern, und danach wieder HINWEISROT instanziieren, dann hat dieses Fenster ebenfalls die neue Größe. D.h. Änderungen an der übergeordneten Klasse (der sog SUPERCLASS ) wirken sich also auf die abgeleiteten Unterklassen aus. Der Flachmann sagt dazu:

oder auch Inheritance. Die Subklasse erbt sozusagen alle Eigenschaften des Vaters (oder der Mutter? Heißt ja eigentlich die Klasse...) Bei der Erstellung neuer Klassen werden zuerst einmal alle Methoden und Properties (Funktionen und Variablen) der beerbten Klasse übergeben. In der neuen Subklasse kann man nun gezielt die Punkte abändern, die anders sind.

Durch Neudefinition einer Methode wird die Originalmethode übersteuert (überschreibende Vererbung genannt), oder durch zusätzliche Methoden/Properties wird es eine Erweiternde Vererbung.

In dem CLICK unseres roten Fensters könnten wir z.B ein WAIT WINDOW reinsetzen, das beim Drücken der Schaltfläche auftaucht. Also:

Damit haben wir den Originalcode überschrieben, unser Maske wird nicht mehr beendet, dafür aber haben wir unser schniekes WAIT-Fensterchen. (Und Gottseidank auch noch den Schließknopf in der Titelzeile). Was also tun, wenn man beides haben will? Entweder den alten Code duplizieren (Bah!) oder den Code der Superklasse zusätzlich ausführen. Dies erreicht man durch Einfügen von

als letzte Zeile in unserer Prozedur. Der :: Operator verweist auf das Objekt in der übergeordneten Klasse. Und schon wird unsere Form wieder geschlossen. Dies erreichen wir übrigens durch Aufruf der RELEASE Methode, die ebenfalls immer der Form von Haus aus drin ist. Der Kürzel THISFORM ist identisch zu THIS, nur daß er anstatt aufs aktuelle Objekt auf die aktuelle Form verweist.

Bleibt uns bei unserer Exkursion nur noch der

Tja, da wird´s in der deutschen Sprache schwierig: Vielgesichtigkeit sagt mein allwissendes Nachschlagewerk... Bleiben wir also lieber beim Original.

In jeder Klassendefinition gibt es sog. Standard-Methoden, die eigentlich jede Klasse, jedes Objekt besitzt. Durch den Befehl <Objekt>.SHOW kann man mit einem generischen Befehl z.B. jedes Objekt zum Anzeigen bewegen. Wie und was genau in der SHOW Funktion dann abläuft, ist, kann total unterschiedlich sein. Also: Ein Befehl - Viele Wirkungen: Polymorphismus. Schönes schwieriges Wort für eine Selbstverständlichkeit. Am besten schnell wieder vergessen...

Bei der Programmierung mit Objekten gibt es aber auch ein paar Stolpersteine: Bis jetzt waren Sie es gewohnt, im Initialisierungsbereich einer Maske eine Variable zu definieren (in unserem Beispiel die X). Das selbe könnten Sie auch in der INIT-Methode eines Objektes machen, werden aber sehr bald feststellen, daß die Variable x in anderen Methoden nicht mehr vorhanden ist. Warum?

In der prozeduralen Programmierung haben wir es mit dem "Top-Down" Programmfluß zu tun, einzelne Prozeduren werde hierarchisch von oben nach unten durchlaufen. Unser X ist also in späteren Programmstellen immer existent.

 

Im Gegensatz dazu sind die Methoden eines Objektes alle gleichwertig, quasi nebeneinander. Dadurch erklärt sich, daß eine Variable, die im INIT definiert wird, wieder vorloren geht, wenn man das INIT wieder verlässt. Abhilfe? Nein, keine PUBLIC Variable!! (Pfui!). Sondern Umsteigen auf Properties. Wenn sie anstelle der Variable eine Property in der Form erstellen, können alle Methoden sauber und einfach darauf zugreifen. Sie werden sogar feststellen, daß man wesentlich weniger Variablen benötigt als bisher.

Bis jetzt hatten wir ja mit visuellen, sichtbaren Klassen gearbeitet. Nun besteht aber ein Programm nicht nur aus sichtbaren Elementen, sondern auch aus normalen Verarbeitungsroutinen. Und dafür gibt es die virtuelle, nichtsichtbare Klasse. Anstelle der Basisklasse FORM verwenden wir jetzt die Klasse CUSTOM.

Mehrere Gründe sprechen dafür, seine gelibten Verarbeitungsroutinen nun als Klassen zu programmieren:

Sauber abgeschlossene Umgebung; kein Überschneiden mit anderen Variablen

Jedes Objekt hat seinen eigenen Errorhandler! Vorbei die Zeiten eines globalen Fehlerabfangs, der meist doch nichts anderes als einen restart durchgeführt hat. Nun können Sie gezielt auftretende Fehler bekämpfen, da sie genau wissen wo er aufgetreten ist: im aktuellen Objekt!

Desweiteren können Sie nun von überall auf eine solches Objekt zugreifen, seine Properties lesen und schreiben und die Methoden ausführen.

Prinzipiell hat jedes CUSTOM Objekt drei wichtige Methoden: INIT, DESTROY und ERROR. Der INIT Code wird beim Instanziieren abgearbeitet, und durch die DESTROY Methode haben sie eine saubere Möglichkeit, beim Beenden eines Objektes (oder Routine) jedweden Code auszuführen wie zB Werte zurücksetzen, Daten abspeichern usw.

Wie oft haben Sie folgenden Code schon geschrieben:

Zum Einen verwenden wir nen Haufen globale Variablen, zum anderen ist´s ein aufwendiger Modus. Jede SET Einstellung braucht drei Zeilen, und dies in jedem Programm wieder! Als Klasse würde man die gc* Variablen als Properties anlegen, die INIT Methode führt das Setzen der Einstellungen aus, und der Destroy setzt automatisch beim Beenden wieder alles zurück. Die eigentliche Verarbeitung wird als neue Methode eingebaut, sodaß der Aufruf dann do aussehen könnte

Und der dazugehörige Code: