Objektorientiert: Unterschied zwischen den Versionen

Aus Wiki des Deutschsprachige Xbaseentwickler e. V.
Zur Navigation springen Zur Suche springen
Keine Bearbeitungszusammenfassung
Zeile 6: Zeile 6:
  (Dialog zweier unbekannter Programmierer)
  (Dialog zweier unbekannter Programmierer)


Clipper brachte mit TBrowse() und Error() die ersten Objekte ins Spiel. Zu dieser Zeit war es nicht möglich, eigene Objekte zu definieren (und zu verwenden). Mittels Class(y) konnte man allerdings eigene Objekte erstellen und verwenden.
Clipper brachte mit [[TBrowse()]] und [[Error()]] die ersten Objekte ins Spiel. Zu dieser Zeit war es nicht möglich, eigene Objekte zu definieren (und zu verwenden). Mittels Class(y) konnte man allerdings eigene Objekte 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).
Ein Objekt ist eine Kombination aus Daten (= Instanzvariablen) und Programmcode (= Methoden), die einem bestimmten Zweck dienen (oder auch mehreren, je nach Definition).
Zeile 39: Zeile 39:
Ob man eigene Klassen einsetzt, ist mehr oder weniger eine Geschmacksfrage (oder eine Frage der  
Ob man eigene Klassen einsetzt, ist mehr oder weniger eine Geschmacksfrage (oder eine Frage der  
Vorgaben).
Vorgaben).


== self: ==
== self: ==

Version vom 15. Juni 2013, 08:10 Uhr

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 nicht möglich, eigene Objekte zu definieren (und zu verwenden). Mittels Class(y) konnte man allerdings eigene Objekte 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).

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 wird:

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.


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.


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.


(Artikelinarbeit)