Objektorientiert

Aus Wiki des Deutschsprachige Xbaseentwickler e. V.
Zur Navigation springen Zur Suche springen

Objekte

"Was ist ein Objekt?"
"Das, was man draus macht."
(Dialog zweier unbekannter Programmierer)

Clipper brachte mit TBrowse() und Error() die ersten Objekte ins Spiel. Zu dieser Zeit war es in Clipper nicht möglich, eigene Klassen zu definieren (und zu verwenden). Mittels Class(y) konnte man allerdings eigene Klassen definieren und Objekte daraus erstellen und verwenden.

Ein Objekt ist eine Kombination aus Daten (= Instanzvariablen) und Programmcode (= Methoden), die einem bestimmten Zweck dienen (oder auch mehreren, je nach Definition).

Der Vorteil eines Objektes ist, dass es sich dabei einmal um "gekapselten" Code handelt, auf den man von aussen keinen Einfluss nehmen kann (ausser dem, den der Programmierer vorgesehen hat).

Dieser Code kann (normalerweise) auch von einem Programm zum nächsten mitgenommen werden und sollte - im Idealfall - einfach neu verwendet (re-used) werden.


Zum anderen stellen die Instanzvariablen auch eine gute Dokumentation dar.

nErgebnis := TueIrgendEtwas(nValue, nPieces, nPrice, nCount, cName, @nBonus)

Dieser Funktionsaufruf vermittelt einen Eindruck (!) davon, was TueIrgendEtwas() machen könnte. Um das genau festzustellen, muss man aber einen Blick in den Quellcode von TueIrgendEtwas() werfen.

oClass := TueIrgendEtwas():new()
oClass:Menge    := nValue
oClass:Anzahl   := nPieces
oClass:EKPreis  := nPrice
oClass:Position := nCount
oClass:ArtName  := cName
oClass:calculate()
nErgebnis := oClass:Positionspreis
nBonus    := oClass:Positionsrabatt

Eine Dokumentation setzt immer voraus, dass Zeit (und Lust) dafür vorhanden sind. Wenn die Instanzvariablen einer Klasse sauber beschrieben sind, muss man zum einen nicht befürchten, Parameter in ihrer Reihenfolge vertauscht zu haben, sondern man erkennt die Bedeutung eines Parameters.

Klar, man könnte alle Parameter auch an :calculate() übergeben, damit würde man von der Parameter-Bedeutung aber nicht unbedingt viel gewinnen.

Dazu kommt, dass der Aufruf einer Methode generell ein Ergebnis zurückliefert (wie jede andere Funktion auch), aber wir können Instanz-Variablen benutzen, um weitere Ergebnisse bereitzustellen. Man denke nur an die drei ersten Parameter von AppEvent(), die per reference übergeben werden müssen, um mehr als das Standard-Ergebnis zu erhalten.

Ob man eigene Klassen einsetzt, ist mehr oder weniger eine Geschmacksfrage (oder eine Frage der Vorgaben).


Deklarationsschlüsselwörter

READONLY

Es besteht die Möglichkeit, Instanzvariablen explizit als schreibgeschützt zu definieren. In einem solchen Fall kann die entsprechende Instanzvariable nur innerhalb der Methoden der Klasse verändert werden.

Versuche, eine als READONLY definierte Variable ausserhalb einer Methode der entsprechenden Klasse zu verändern, führt zu einem Laufzeitfehler:

CLASS ShowIt
   VAR oDlgParent   READONLY
   ...
ENDCLASS

FUNCTION Testen(oDlg)
   Local oShow
   oShow := ShowIt():New()
   oShow:oDlgParent := oDlg


EXPORTED:, PROTECTED:, HIDDEN:

Diese Schlüsselwörter gelten ab ihrem Auftreten im Quellcode bis zum Ende der Klassen-Deklaration, oder bis eines der anderen Schlüsselwörter verwendet wird.

Standardmässig sind alle Instanzvariablen und Methoden einer Klasse HIDDEN: und somit nur innerhalb der Methoden nutzbar. Sie sind auch nicht sichtbar in abgeleiteten Klassen.

PROTECTED: ist ähnlich wie HIDDEN:, nur hier sind die Instanzvariablen und Methoden auch in abgeleiteten Klassen sichtbar.

Das Schlüsselwort EXPORTED: weist den Compiler an, die folgenden Instanzvariablen und Methoden auch ausserhalb der Klasse sichtbar zu machen.




Instanzvariablen

Eine Instanzvariable ist ein Variable, die durch die Klasse definiert und im Objekt bereitgestellt wird:

oXbp := XbpDialog():new(oArea, oArea, aPos, aSize)
oXbp:title := "Mein erstes Xbase-Programm

In diesem Beispiel ist :title eine Instanzvariable, die seitens des Programmierers verändert werden kann.

Erzeugt wird eine Instanzvariable durch die Verwendung des Schlüsselwortes VAR bei der Deklaration einer Klasse:

CLASS ShowIt
   EXPORTED:
   VAR oDlg         
   VAR oDlgParent   READONLY


Methoden

Als Methode bezeichnet man eine Funktion, die zu einer Klasse gehört und dazu dient, bestimmte Aufgaben im Zusammenhang mit der Klasse auszuführen.


self:

self: ist eine Variable, die implizit in jeder Methode einer Klasse vorhanden ist. Sie erlaubt den Zugriff auf Instanzvariablen und Methoden der Klasse. Da self implizit deklariert wird, führt eine weitere Deklaration zu einem Fehler beim Compilieren.

Dies ist auch der Grund, warum im Event-Loop keine Referenz auf das bezogene Objekt an :handleEvent() weitergegeben werden muss:

nEvent := AppEvent(@mp1, @mp2, @oXbp)
oXbp:handleEvent(nEvent, mp1, mp2)

Innerhalb der Methode :handleEvent() ist über self: ein Zugriff auf das bezogene Objekt möglich.

Als Kurzform für self: kann auf :: verwendet werden.


Der Nachrichten-Operator :

Im Zusammenhang mit Objekten wird ein eigener Operator verwendet, der Doppelpunkt. Der Doppelpunkt besagt, dass Bezug genommen wird auf das rechts stehende Objekt, und zwar entweder auf eine Instanzvariable, oder auf eine Methode (wobei die Methode daran erkennbar ist, dass auf ihren Namen ein Paar runde Klammern folgt.

Die Verwendung des Nachrichten-Operators auf eine Variable (oder den Rückgabewert einer Funktion), die nicht vom Typ Objekt sind, führt zu einem Laufzeitfehler.

Die Bezugnahme einer nicht existierenden Instanzvariablen oder Methode führt ebenfalls zu einem Laufzeitfehler.

Der Nachrichten-Operator zeigt aber auch, dass Objekte nicht so "fest gefügt" sind, wie es oft erscheint. Der Compiler nimmt eine Querprüfung vor, dass deklarierte Methoden auch implementiert sind, bzw. dass implementierte Methoden auch deklariert sind:

CLASS DemoFehler
   EXPORTED:
   METHOD Felher
ENDCLASS

METHOD DemoFehler:Fehler()
   //
RETURN(self)

Dieser Code führt zu zwei Fehlern:

  • die deklarierte Methode Felher ist nicht implementiert;
  • die implementierte Methode Fehler ist nicht deklariert.

Damit endet aber auch die Prüfung einer Klasse, mit der Sicherstellung, dass die Methoden vollständig sind. Zur Laufzeit werden Nachrichten an das Objekt gesendet, und das Objekt prüft in einer internen Tabelle, ob es die angesprochenen Instanzvariable bzw. Methode gibt oder nicht.

Aufgrund dieser Eigenschaft kann eine Instanzvariable (oder Methode) auch über den Macro-Operator angesprochen werden:

cVarName := "Status"
oObj:&cVarName

Umgang mit nicht existierenden Instanzvariablen und Methoden

Es ist möglich, Instanzvariablen und Methoden eines Objektes anzufordern, die nicht existieren (hauptsächliche Ursache: Schreib- oder Gedächtnisfehler).

Neben der Option eines Laufzeitfehlers bietet Xbase++ aber auch die Möglichkeit, dass das Programm auf diese Situation reagiert. Die Basis-Klasse Abstract() stellt hierfür drei Methoden zur Verfügung, die den Umgang mit solchen Situationen erleichtern:

:noMethod() wird ausgeführt, wenn eine Methode angefordert wird, die im Objekt nicht verfügbar ist.

:getNoIVar() wird ausgeführt, wenn eine Instanzvariable angefordert wird, die im Objekt nicht verfürbar ist und erlaubt es dem Programmierer, einen Wert zurückzuliefern.

:setNoIVar() wird ausgeführt, wenn eine Instanzvariable verändert werden soll, die im Objekt nicht verfürbar ist.


Vererbung

Klassen kann man vererben, oder genauer gesagt, man kann von einer Klasse (auch von mehreren Klassen) neue Klassen ableiten.

Die abgeleiteten Klassen "erben" erst einmal den Aufbau der ursprünglichen Klasse (hier kann durchweg der Plural gesetzt werden, wenn ich im folgenden aus Gründen der Vereinfachung den Singular verwende).

Ziel kann z.B. eine kombinierte Klasse sein. Ein Beispiel dafür ist die XbpComboBox() Klasse, die sich darstellt als eine Kreuzung der XbpListBox() Klasse mit der XbpSLE() Klasse.

Ein anderes Ziel ist es, zusätzliche Methoden oder Instanzvariablen zu implementieren, die man über die vorhandenen Methoden oder Instanzvariablen hinaus benötigt:

Die Instanzvariable :cargo ist sicherlich ein interessantes Vehikel, man stösst aber gerade bei komplexeren Anwendungen oft an die Grenzen, was man :cargo zumuten kann. Dann macht es Sinn, bestimmte Klassen abzuleiten und in der eigenen Klasse dann zusätzliche Instanzvariablen zu definieren, um Informationen zu speichern, die für die Verarbeitung wichtig sind.

Vererbung ist aber nicht nur ein Weg, Probleme zu lösen, sondern auch, sie zu schaffen:

XbpComboBox() ist eine Klasse, die von XbpListBox() und XbpSLE() abgeleitet ist. Beide Basis-Klassen verfügen über diverse Methoden und Instanzvariablen gleichen Namens.

Besonders kritisch ist hier :getData(). Die XbpListBox:getData() Methode liefert ein Array zurück, während XbpSLE:getData() einen String zurückliefert. Da der Focus der XbpComboBox() auf der Auswahl aus der Liste liegt, liefert XbpComboBox:getData() ein Array zurück, und zwar das Ergebnis des XbpListBox:getData().

Was ist aber, wenn der eingegebene String benötigt wird? In diesem Fall kann durch den Verweis auf die Basis-Klasse deren Methode ausgeführt werden:

self:XbpSLE:getData()

Mit dieser Anweisung wird das (implizite) XbpSLE()-Objekt der XbpComboBox()-Klasse angesprochen, und dann die Methode :getData() von XbpSLE() ausgeführt.


super:

Das Konzept von super: ähnelt dem von self:, macht aber nur Sinn in Klassen, die von anderen Klassen abgeleitet sind.

self: bezieht sich immer auf die ausgeführte Klasse (und die darin definierten Methoden und Instanzvariablen). super: erlaubt es, in einer Methode Bezug auf Methoden und Instanzvariablen zu nehmen, die in der Klasse definiert sind, deren Methode gerade ausgeführt wird.

Verwirrt? Ich auch.


vordefinierte Methoden zwecks Vererbung

Im Bereich des OwnerDrawing gibt es beispielsweise die Methode :customDrawCell im XbpMultiCellGroup() Objekt. Diese Methode führt keinen Code aus, sondern ist dafür gedacht, in eigenen, abgeleiteten Klassen das OwnerDrawing über diese Methode zu implementieren.


Klassen-Variablen und -Methoden

Um die Bedeutung von CLASS VAR und CLASS METHOD zu verstehen, muss zuerst noch einmal auf die Standards VAR und CLASS eingegangen werden.

Die Klasse ist quasi eine Vorlage, von der durch die Methode new() eine Kopie erstellt wird. Gleichzeitig wird die Methode :init() ausgeführt, um sicherzustellen, dass bestimmte Instanzvariablen einen definierten Ausgangswert haben. Ab dem Moment, wo die Kopie erstellt und als Referenz einer Variablen zugewiesen ist, sind die dort vorhandenen VAR und METHOD "auf sich selbst gestellt". Es ist nicht möglich, aus einem Objekt der Klasse XbpDialog() auf eine Instanzvariable eines anderen Objektes der gleichen Klasse zuzugreifen, normale VAR sind quasi "LOCAL" für die jeweilige Kopie (auch wenn ihre Sichtbarkeit die komplette Klasse umfasst).


Klassen-Variablen

Eine CLASS VAR hingegen ist nicht Bestandteil eines Objektes, sondern der Klasse. Jedes Objekt hat eine eigene Implementierung jeder VAR, aber nicht der CLASS VAR.

Wozu benötigt man eine Instanzvariable, die nur auf Klassen-, aber nicht auf Objekt-Ebene vorhanden ist?

Ein Beispiel soll das erklären:

Für DBF-Dateien wird ein implizierter dbCloseAll() am Programmende ausgeführt. Arbeitet man stattdessen z.B. mit dem MySQL-Wrapper, fehlt diese Funktionalität.

In der Definition der MySQL-Klasse sieht das (in meiner Fassung) wie folgt aus:

  CLASS MySql
  EXPORTED:
  CLASS VAR aConnections
  VAR pMySql, aDBases , cDb , cServer ,cUser, cPassWord ,cDbName ,nPort ,cSocket,nFlag,lOK
  VAR cDatabase ,aTables
  //---------------------------------------------------------------------------//
  INLINE METHOD Init()
  IF self:aConnections == NIL
     self:aConnections := {}
  ENDIF
  if Empty( ::pMySql )
     ::pMySql := 0
  else
      MySQLError(::pMySql, "See MySqllib.dll")
  return 0
  end
  ::pMySql := mysql_init(::pMySql)
  AAdd(self:aConnections, {::pMySql, {ProcName(1), ProcName(2), ProcName(3)}, ThreadObject()})
  return Self

Es existiert eine CLASS VAR aConnections. Beim Aufruf der init() Methode wird sichergestellt, dass diese CLASS VAR den Typ Array hat.

Jedesmal, wenn eine neue Verbindung hergestellt wird, wird eine Reference auf die Verbindung, die ersten drei Einträge aus dem Call-Stack, sowie eine Referenz auf den aktuellen Thread als weiterer Eintrag im Array abgelegt.


 INLINE METHOD Close()
 Local lClose
 Local nI, nLen
 lClose := mysql_close(::pMySql)
 nLen := Len(self:aConnections)
 FOR nI := 1 TO nLen
    IF self:aConnections[nI, 1] == ::pMySql
       ADel(self:aConnections, nI)
       ASize(self:aConnections, nLen - 1)
       EXIT
    ENDIF
 NEXT
 self:pMySql := NIL
 return lClose

Beim expliziten Schliessen einer Verbindung wird geprüft, welche der gespeicherten Verbindungen geschlossen wurde, und der entsprechende Eintrag wird aus der CLASS VAR entfernt.

Damit enthält die CLASS VAR immer eine Liste der aktiven MySQL-Verbindungen und kann von jedem Objekt der MySQL-Klasse abgefragt werden.

Klassen-Methoden

Klassen-Methoden sind an die Klasse gebunden und nicht an die davon abgeleiteten Objekte und können daher nur auf Instanzvariablen zugreifen, die als CLASS VAR definiert sind. Der Zugriff auf normale VAR führt zu einem Laufzeitfehler.

Greifen wir das vorherige Beispiel auf, und definieren eine CLASS METHOD und eine normale METHOD:

 CLASS METHOD closeClass
    INLINE METHOD Close()
    Local lClose
    lClose := mysql_close(::pMySql)
    self:closeClass(::pMySql)
    self:pMySql := NIL
 RETURN lClose
 
 CLASS METHOD MySQL():Close(oCon)
 Local nI, nLen
 nLen := Len(self:aConnections)
 FOR nI := 1 TO nLen
    IF self:aConnections[nI, 1] == oCon
       ADel(self:aConnections, nI)
       ASize(self:aConnections, nLen - 1)
       EXIT
    ENDIF
 NEXT
 RETURN(.T.)

Da die CLASS METHOD nicht auf "normale" Instanzvariablen zugreifen kann, muss der MySSQL-Handle :pMySql als Parameter an die CLASS METHOD übergeben werden.


Die Untersuchung eines Objektes

Jede Klasse verfügt über bestimmte Methoden, die es erlauben, Informationen über die Klasse zu erhalten.

:className() liefert den Namen der Klasse zurück, wie sie definiert wurde. Das bedeutet, dass Gross- und Kleinschreibung nicht verändert wird:

cClassName := oXbp:className()
IF cClassName = "XBPDialog"

Wenn oXbp ein Objekt der Standard-Klasse XbpDialog() ist, wird der Vergleich fehlschlagen, da der Rückgabewert von :className() in diesem Fall "XbpDialog" lautet. Eine Variante der Abfrage wäre dann:

cClassName := oXbp:className()
IF Upper(cClassName) = "XBPDialog"

classDescribe() liefert ein Array zurück, das folgende Informationen enthält:

  • Name der Klasse
  • Klassen, von denen diese Klasse abgeleitet ist
  • Liste der Instanzvariablen
  • Liste der Methoden

Das Wissen um das Vorhandensein von nicht dokumentierten Instanzvariablen und Methoden bedeutet nicht zwangsweise, dass diese zur Verwendung freigegeben sind - nicht zuletzt, weil eine Dokumentation über die genaue Funktionsweise nicht vorliegt. Dazu kommt, dass es keine Gewähr gibt, dass diese Instanzvariablen und Methoden in einem kommenden Release noch vorhanden sind.