![]() |
![]() |
Der Begriff "OVP" = "Objektverarbeitend" stammt von Arne Schäpers und Rudolph Huttary, Redakteure bei der Zeitschrift c't. Ich finde ihn sehr gut, weil er die Realität beschreibt, in der zwar heute echte OOP-Sprachen wie C#, Python oder Groovy als das Ei des Progammierer-Kolumbus präsentiert werden, in der die Anwender in der Praxis aber oftmals nur eine Untermenge dieses Konzepts verwirklichen - und das oft nicht aus Gründen der Unwissenheit, sondern des Pragmatismus.
In diesem Kapitel soll auf diese Untermenge eingegangen werden: Es werden Klassen, Objekte, Methoden besprochen und es wird dann auf Objekthierarchien und Objektbäume eingegangen. Nein, nicht Klassenhierarchien und Klassenbäume - das kann Freebasic bisher nicht und das gehört auch nicht in die OVP, sondern in die OOP. Auch auf Klassenpolicies (private, public) wird verzichtet. Genauso wird auch nicht auf Ereignisbehandlung, generische Klassen, Interfaces, Mehrfachvererbung und der "Schikanen" mehr eingegangen - das bleibt späteren Kapiteln mit anderen Sprachen vorbehalten. Dass wir auf Containerklassen nicht verzichten müssen, haben wir in den vorhergehenden Kapiteln schon gelernt.
Die Objektverarbeitung muss bei Freebasic zwangsläufig funktionieren, da Freebasic ja mit objektorientierten Klassenbibliotheken zusammenarbeitet. Sie setzt keine speziellen Sprachelemente voraus, sondern kommt mit dem aus, was wir bisher gelernt haben. Eigentlich haben wir die Konzepte in den beiden vorhergehenden Kapiteln schon benutzt, wir haben das nur noch nicht richtig thematisiert.
Objekte sind im Grunde nichts anderes als die Verbundvariablen ("User Defined Types" = UDT), die wir schon kennen. Wir haben ja beiläufig sie auch schon immer mal wieder so bezeichnet. Ihre Typdefinitionen heissen "Klassen". Allerdings ist das noch nicht ganz alles. Der Trick bei der Weiterentwicklung der Informatik ist ja immer die gekonnte Einschränkung. Die Einführung von etwas, was man nicht machen darf. Paradoxerweise ist das oft ein grosser Gewinn. Im Fall der OOP und OVP werden den Verbundvariablen Funktionen zugewiesen. Und nur diese Funktionen sind berechtigt, auf die Elemente der Verbundvariablen zuzugreifen. Lassen Sie uns das gleich an einem Beispiel demonstrieren:
TYPE tclass1 ' Ich bin eine Klasse x AS DOUBLE y AS DOUBLE END TYPE SUB tclass1_setx(obj AS tclass1, x0 AS DOUBLE) 'Nur ICH darf auf tclass1 zugreifen WITH obj .x=x0 END WITH END SUB SUB some_other_function() obj.y=3 'DAS IST VERBOTEN! END SUB
tclass1_setx() wurde vom Namen her als Funktion gekennzeichnet, der der Zugriff auf Objekteelemente des Typs tclass1 erlaubt ist. Man bezeichnet die Funktionen, die zugreifen dürfen, als die Methoden der Klasse. Weiter unten wurde ein Beispiel angeführt, das zwar in Freebasic funktionieren würde, nicht aber in einer echten OOP-Sprache und das dem Prinzip der OVP/OOP widerspricht: Elemente der Objekte dürfen nur von Methoden der eigenen Klasse, von nichts anderem manipuliert werden. In der strengen OOP dürfen die Elemente noch nicht einmal von ausserhalb gelesen werden.
Durch die konsequente Benutzung von WITH nur für die Objekte, denen die Methode zugeordnet ist, kann man in Freebasic dieses Konzept noch stärker hervorheben. Der führende Punkt bezeichnet dann immer ein Element des Objekts. Man sagt übrigens bei Objekten nicht "Element", sondern "Eigenschaft". Am obigen Beispiel: tclass1 ist die Klasse, obj ist ein Objekt, x und y sind die Eigenschaften des Objekts, tclass1_setx() ist eine Methode der Klasse tclass1. Wenn wir WITH wie beschrieben einsetzen, dann bedeutet der führende Punkt, dass es sich um eine Eigenschaft des Objekts handelt.
Warum diese ganzen Beschränkungen? Es ist ähnlich wie bei der prozeduralen Programmierung, bei der der Zugriff auf die lokalen Variablen beschränkt wird: Kapselung. Die Lösung einer Aufgabe, die Definition eines Verhaltens wird abgekapselt von der Objekt-Umwelt, von ihr unabhängig gemacht. Wir haben das bei der Beschäftigung mit den Listen gemerkt: Was wir hier eigentlich gebastelt haben, waren Listenobjekte. Und es gilt nun: Was im Innern des Objekts passiert - sprich: in seinen Methoden und Eigenschaften - ist völlig unabhängig von dem Kontext, in dem die Liste benutzt wird. Gibt es einen Fehler bei der Listenverarbeitung, ist es ein allgemeiner Fehler, der nichts mit der speziellen Benutzung zu tun hat. Auch muss sich der Anwender um die Frage, welche Methoden angestossen, welche Daten noch herangezogen werden müssen usw., nicht kümmern. Das Ergebnis dieses Prinzips ist schon bestechend, weil es dafür sorgt, dass hierarchisch aus Objekten aufgebaute Programme einen fast beliebigen Grad an Komplexität erreichen können. Ein moderner Internet-Browser, ein modernes Betriebssystem, ein 3D-Grafikprogramm oder ein modernes Spiel sind die beeindruckenden Nachweise hierfür. Ohne OOP oder zumindest OVP wäre dies nicht möglich.
Jede Klasse muss eine Methode haben, die sie initialisiert - den Konstruktor. Falls dynamische Variablen vorkommen, muss sie auch einen Destruktor haben, der den Speicherplatz wieder freigibt. Wir können zu den beiden auch init()-Methode und close()-Methode sagen. In der echten OOP werden die Konstruktoren und Destruktoren immer automatisch aufgerufen. Allerdings hat das nicht nur Vorteile. Hier in Freebasic müssen wir die init()- und close()-Routinen manuell aufrufen. Das sollten wir allerdings generell und immer machen, selbst wenn zunächst gar nichts zu initialisieren oder freizugeben ist.
Richtig interessant wird die OVP erst dann, wenn Objekthierarchien verwendet werden. Wir haben am Schluss von Teil I einen kleinen 3D-Kran entworfen. Stellen wir uns vor, wir hätten da weitergemacht und das Ganze zu einer virtuellen Baustelle ausgebaut, auf der der Anwender Lastwagen, Kräne, Bagger u.v.m bewegen und steuern kann. Kompliziert umzusetzen? Nun, prinzipiell nicht. Alles, was wir bräuchten, wäre ein paar Fahrzeugklassen "tcrane", "tdigger", "ttruck" usw. und ein paar Methoden, um diese zu steuern: "tcrane_turnleft", "tdigger_moveaxe()" usw. Oder wir haben nur eine Fahrzeugklasse "tcar", die alle notwendigen Methoden bereitstellt, deren Aufruf gültig ist je nach Typ, mit dem tcar initialisiert wurde - als TDIGGER, TCRANE, TTRUCK oder sonst was.
Aber was innerhalb der Objekte geschehen muss, ist gewaltig. Wenn wir alle Fahrzeuge aus Quadern zusammensetzen, wie wir das beim Kran gemacht haben, dann wäre z.B. eine Realisierung, die Geometrie in einer Liste aus Quadern festzuhalten. Allerdings brauchen wir nicht nur diese Liste, sondern auch noch eine, die die Bewegungsgeometrie festhält. Das könnte z.B. eine Liste von Achsen sein. "Achse" wäre dann ebenfalls eine Klasse. So wie "Quader". Eine Achse wiederum müsste allerdings wissen, welche Quader mit ihr verbunden sind und sich mitdrehen. Das können wir als Liste von Zeigern auf Quader definieren. Wie unsere Quader aufgebaut ist, wissen wir im Grunde ja schon: cube2typ haben wir die "Klasse" damals genannt. (Sie hat die Drehachse allerdings schon beinhaltet, das war nach jetzigem Wissensstand nicht gut). cube2typ wiederum enthält als Kindobjekt eines der Klasse cubetyp, das die acht Eckpunkte enthält, die wiederum aus der Klasse point3dtyp stammen:
Wir haben es also hier mit einer Objekthierarchie zu tun: Objekte des Typs point3dclass sind Eigenschaften der Klasse tcubetyp. Wenn ein Objekt Eigenschaft eines anderen Objekts ist, dann sprechen wir von "Kind" und "Eltern". (Wobei hier Eingeschlechtlichkeit gilt: Ein Kindobjekt kann nur ein Elternobjekt besitzen.)
Soweit haben wir bisher eigentlich nichts Neues gelernt. Das einzige Neue, was in diesem Kapitel angesprochen wird, sind die Baumbeziehungen zwischen Kind und Eltern. In Objekthierarchien ist es wichtig, dass dem Kind immer bekannt ist, zu welchem Elternobjekt es gehört. Daher ist es sehr wichtig und sehr fruchtbar, in Objekthierarchien grundsätzlich Elternzeiger mitzuführen:
TYPE cube2typ parent AS geometryclass PTR 'Hier wird angezeigt, zu welchem Geometrieobjekt der Quader gehört! cube AS cubetyp x1 AS DOUBLE x2 AS DOUBLE x3 AS DOUBLE col1 AS INTEGER flag AS INTEGER move AS STRING * 10 nmove AS INTEGER END TYPE
Auf diese Weise fällt es z.B. nicht schwer, den Quader in derselben Figur zu finden, der mit "mir", dem betrachteten Quader, eine gemeinsame Seite hat. Allgemeines Prinzip:
Natürlich kann und muss man manchmal Hierarchien auch noch mit mehr Zeigern verknüpfen als nur mit parent-Zeigern. Haben wir es z.B. mit statistischen Daten zu Haushalten zu tun, dann sind Zeiger auf das jeweilige Ehepartnerobjekt sehr wichtig. Datentechnisch gesehen ist der Ehepartner ansonsten nur ein beliebiges weiteres Element einer flachen Objektliste. Man sollte solche zusätzlichen Zeiger allerdings nur einführen, wenn erstens der Zeiger statisch für die gesamte Programmlaufzeit gültig bleibt (und auch die Bedeutung die gleiche bleibt) und wenn zweitens das Ziel nicht über einen einfachen Zeigerpfad zugänglich ist.
Grundsätzlich gilt also, Zeigerpfade durch den Baum zu erhalten und nicht durch obstruse Abkürzungszeiger zu verwirren. Gerade dann, wenn Zeigerpfade lang sind, sollten sie nach Möglichkeit erhalten bleiben. Dies sieht zwar zunächst abschreckend aus, erhöht aber dann die Lesbarkeit des Programms beträchtlich. Allzulange Pfade kann man z.B. auch durch lokal definierte Zeigervariablen abkürzen:
SUB tclass1_method(tclass1 obj) DIM AS tclass1_grandchild px px=obj->child->grandchild y=tclass1_granchild_method(px) z=px->x+y ... END SUB
So etwas ist noch nicht die Sicht verdeckend, da die Definition von px nahe bei seiner Verwendung vorgenommen wurde, der Leser also mit einem kurzen Blick erschlossen hat, um was es sich bei px genau handelt.
Übung 1:
Schreiben Sie das 3D-Kranbeispiel aus Teil2 so um, dass es konsequent OVP-strukturiert ist.