OOP in Delphi

Gespeichert von Lemmy am So., 16.09.2018 - 19:43

 

Jeder, der das obligatorische "Hello World" Programm in Delphi erstellt hat, ist schon mit Klassen und Objekten in Berührung gekommen, er hat schon objektorientiert programmiert. Im folgenden Tutorial werde ich die Grundlagen von OOP erläutern und dabei auf die Besonderheiten von Delphi eingehen. Als Abschluss kommt dann ein kleines Beispielprojekt.

Klasse und Objekt - alles das selbe oder doch nicht? Und was ist eine Instanz?

Oft wird der Begriff "Klasse" und "Objekt" verwechselt bzw. missverstanden. Zwischen beiden besteht ein großer Unterschied, dieser ist aber sehr einfach zu merken: Das Verhältnis Klasse zu Objekt ist identisch wie das Verhältnis zwischen Variablentyp und Variable (also z.B. Integer und Variable i). Der Begriff Instanz ist gleichbedeutend mit dem Objekt. Also ist mit einer Instanz einer Klasse ein Objekt gemeint.

Was ist eine Klasse?

Die Klasse ist sozusagen eine Vorlage für Objekte. Ähnlich wie bei den einfachen Datentypen (Integer) muss ein Objekt erst deklariert werden, damit diese benutzt werden kann, also var objekt:TKlasse; Allerdings reicht das bei einem Objekt nicht ganz aus. Das Objekt muss nach der Deklaration erst mal initialisiert werden. Bestimmte Attribute müssen Werte zugewiesen werden usw. Die Initialisierung erledigt der Konstruktor.

Konstruktor und Destruktor

Der Konstruktor und sein Gegenspieler der Destruktor sind spezielle Methoden eines Objektes, die bei der Initialisierung (Konstruktor) bzw. beim Löschen (Destruktor) aufgerufen werden. Im Konstruktor werden z.B. die Konstruktoren anderer Objekte aufgerufen. Der Destruktor gibt beim Löschen den benutzen Speicher wieder frei, indem er die erzeugten Objekte wieder freigibt. Eine Klasse kann man grundsätzlich mit einem Record verglichen werden. Dort werden unterschiedliche Datenarten zusammengefasst, um diese zu kombinieren. Bei einer Klasse gibt es aber zusätzlich Funktionen und Prozeduren, die die gespeicherten Daten bearbeiten können. In der OOP gibt es 3 Basisprinzipien, durch die die OOP so mächtig wird:

  • Kapselung
  • Vererbung
  • Polymorphie

Auf diese 3 Prinzipien werde ich im folgenden eingehen.

Was ist nun ein Objekt?

Ein Objekt in der Softwareentwicklung ist letztendlich dasselbe wie in der realen Welt (z.B. Auto). Das Objekt hat bestimmte Eigenschaften (wie z.B. Farbe, Höchstgeschwindigkeit) und das Objekt kann bestimmte Tätigkeiten bzw. Befehle ausführen (z.B. starten, beschleunigen, bremsen). Also: Ein Objekt besteht aus verschiedenen Methoden (Prozeduren und Funktionen) und Attributen (Variablen, die Daten enthalten). Die Attribute werden dabei durch die Methoden sozusagen geschützt, d.h. dass nur über eine Methode auf ein Attribut zugegriffen werden kann. Bsp. Tanken: Beim Auto kann normalerweise nur durch das Tankloch Sprit in den Tank eingefüllt werden. In den Tank passt zudem nur eine bestimmte Menge an Treibstoff. Ist der voll, läuft der Tank einfach über. Als Methode Tanken in einem Objekt würde das ungefähr so aussehen:

procedure TAuto.Tanken(FValue: Currency);
begin
  if maxTankvolumen >= Tankfuellung+FValue then
    FTankfuellung:=Tankfuellung+FValue

  else
    FTankfuellung:=MaxTankVolumen;
end; 

So, nun kann der imaginäre Tank unseres Autos nie mehr überlaufen. Wichtig ist jetzt nur noch, dass man von "aussen" nicht auf das Attribut FTankfuellung zugreifen kann. Sonst wäre ja folgendes ohne Probleme möglich:

FTankfuellung:=2*MaxTankinhalt;

Da das in der realen Welt nicht passieren kann, darf es auch in der abgebildeten Version in unserem PC nicht passieren. Damit das nicht möglich ist, gibt es die sogenannte Kapselung. Kapselung bedeutet, dass nur mit bestimmten Methoden auf Attribute zugegriffen und diese verändert werden können. Es bedeutet schlicht, dass bestimmte Attribute und Methoden vor der Außenwelt versteckt werden. Bei einem Auto interessiert es den Anwender (Autofahrer) überhaupt nicht, was genau passiert, wenn er das Auto betankt, hauptsache er hat nachher Sprit im Tank.

Wie funktioniert die Kapselung?

In der OOP gibt es die Schutzklassen private und public. Wie die Namen schon sagen, versteckt private alle folgenden Attribute und Methoden vor der Öffentlichkeit, während public den vollen Zugriff erlaubt. Was sich hier jetzt erstmal so toll anhört, macht sich nachher in der Entwicklung ziemlich heftig bemerkbar, da für jedes Attribut, das als private deklariert wird, eine oder mehrere Methoden benötigt wird, die den Zugriff auf diese Variable gestattet. Bsp.: Da der Tankinhalt unseres Objektes Autos als private deklariert wurde, brauchen wir eine Methode (Tanken) die den Zugriff auf dieses Attribut erlaubt. Hier können dann z.B. Wertprüfungen (minimal, maximal) vorgenommen werden, um Fehler zu vermeiden. Um den Zugriff auf private Attribute zu vereinfachen, gibt es in Delphi die properties (Eigenschaften). Diese werden so deklariert:

property Tankfuellung:Currency read FTankfuellung;

Nun kann einfach über den Aufruf

Label2.Caption:=CurrToStr(oAuto.Tankfuellung);

der Tankinhalt angezeigt werden. Will man auch einen Schreibzugriff auf die Eigenschaft erlauben muss sie folgendermaßen implementiert werden:

Tipp: Wenn Du folgendermassen vorgehst, übernimmt Delphi dir eine Menge Tipparbeit: Deklariere das property (oder mehrere davon): property Tankfuellung:Currency; Drücke dann STRG+C, Delphi erzeugt dann die entsprechende Variable und Prozedur zum setzen des Attributes: property Tankfuellung:Currency read FTankfuellung write SetTankfuellung;

Delphi kennt noch 2 weitere Schutzklassen protected und published. Im Bereich published können nur Attribute (also Daten) stehen. Diese können dann schon zur Entwurfszeit im Objektinspektor mit Werten belegt werden. Die Schutzklasse protected werde ich später bei der Vererbung erklären.

Vererbung

Bei der Vererbung wird ein sog. Basisobjekt implementiert und von diesem weitere Objekte abgeleitet. Der Vorteil: Diese abgeleiteten Objekte erben alle Attribute und Methoden der Basisklasse, d.h. diese müssen nicht noch einmal implementiert werden. Als Beispiel soll wieder unser Auto dienen. Wir definieren eine Basisklasse TFahrzeug. Diese Klasse hat die Attribute

  • Höchstgeschwindigkeit
  • Reifenanzahl
  • Sitzplätze

und die Methoden

  • beschleunigen
  • bremsen
  • schalten

Durch die Vererbung ist es jetzt möglich die Klasse TKFZ und die Klasse TFahrrad von der Basisklasse TFahrzeug abzuleiten. Die beiden abgeleiteten Klassen können dann die Methoden (beschleunigen, bremsen, schalten) entsprechend ihrer Bauart implementieren. Bei der Klasse TFahrrad werden die Attribute im Konstruktor gesetzt. Nicht so bei der Klasse TKFZ, denn von dieser werden jetzt weiter Klassen TAuto, TBus und TLKW abgeleitet. Bei diesen Klassen müssen jetzt nur noch die Attribute im Konstruktor gesetzt werden. Die Methoden sind bei allen gleich. Es ist also sehr einfach, aus einer allgemein gehaltenen Basisklasse weitere Klasse abzuleiten, die oft auch nur als Vorlage für weitere Vererbungen dienen. Zudem ist es jetzt jederzeit möglich, weitere Klassen von TFahrzeug (z.B. Flugzeug) oder von TKFZ (z.B. Panzer) abzuleiten, ohne dass man wissen muss, wie die Methode beschleunigen funktioniert. Bei der Basisklasse TFahrzeug wurden die Methoden ja nicht implementiert, d.h. es wurden nur die Methodenköpfe (function beschleunigen;) aufgeführt. Diese Methoden müssen in einer abgeleiteten Klasse mit "Leben" gefüllt werden, damit diese funktionieren. Solche Methoden werden als abstrakt bezeichnet, da sie in der Klasse nicht ausgeführt werden können. Solche Klassen, die abstrakte Methoden haben, nennt man dann abstrakte Klassen (logisch). Von diesen Klassen kann dann aber KEIN Objekt abgeleitet werden! Ist auch klar, denn was soll ich mit einem Fahrzeug ohne funktionierende Bremsanlage? Allerdings interessiert das Delphi wenig. Selbst von Klassen die als abstrakt definiert sind, können Instanzen erzeugt werden. Es wird dabei lediglich eine Warnung ausgegeben - also immer auch die Warnungen beachten! Jetzt noch ein Wort zu der Schutzklasse protected. Oben habe ich ja ausgeführt, dass alles was unter private deklariert wird, für alle unsichtbar ist, auch für abgeleitete Klassen! Damit aber diese auf Methoden und Attribute der Elternklasse zurückgreifen können, ohne dass diese von außen einsehbar sind, wurde in Delphi die Schutzklasse protected eingeführt. Alles was hier deklariert wird, ist für Außenstehende unsichtbar, für die abgeleiteten Klassen jedoch sichtbar. Allerdings gibt es eine Einschränkung für private und protected: Wenn innerhalb einer Unit mehrere Klassen definiert werden, können alle Klassen in dieser Unit auf alle in den private/protected deklarierten Felder und Methoden der anderen Klassen zugreifen! Um die private/protected Funktionalität auch in diesem Fall "nutzen" zu können gibt es seit der .NET Unterstützung die strict-Deklaration. Werden Methoden/Attribute als strict private/strict protected deklariert können andere Klassen auch in der selben Unit nicht auf diese zugreifen!

Polymorphie

Die Polymorphie (Vielgestaltigkeit) hängt sehr eng mit der Vererbung zusammen. Wir haben im der Klasse TFahrzeug die Methoden beschleunigen, bremsen und schalten erzeugt. Die abgeleiteten Klassen TKFZ und TFahrrad besitzen diese Methoden, müssen diese aber durch eigene Methoden "überschreiben". Der Aufruf ist aber immer der gleiche! Es ist also egal, ob man ein Objekt der Klasse TFahrrad oder der Klasse TKFZ hat, immer funktioniert der Aufruf objekt.beschleunigen; Bei einem Fahrrad wird eben mehr gestrampelt, bei einem KFZ wird aus Gaspedal getreten. Und genau das ist Polymorphie! Eine Methode mit gleichem Namen wird in abgeleiteten Klassen unterschiedlich implementiert (führt also was anderes aus). So, die grundsätzlichen Dinge sind jetzt geklärt, alles weitere wird sich beim Beispiel erklären.

Das Beispiel Es soll eine Klasse für Flächen implementiert werden. Diese soll die grundsätzlichen Methoden besitzen. Von dieser Basisklasse sollen dann Klassen für Rechtecke und Kreise abgeleitet werden, die die speziellen Methoden für diese Flächen bereitstellen. Die Basisklasse TFlaeche soll folgende Methoden besitzen:

  • berechneFlaeche
  • berechneUmfang

und folgende Eigenschaften

  • Flaeche
  • Umfang

Die abgeleitete Klasse TRechteck erbt von TFlaeche und implementiert zusätzlich folgende Attribute:

  • laenge
  • breite

Die abgeleitete Klasse Kreis implementiert folgendes zusätzliche Attribut:

  • radius

So, jetzt geht's an Delphi. Öffne ein neues Projekt und speichere das ganze gleich ab (Projekt oop_tuto, Unit1 als UMain). Füge dem Projekt eine neue Unit hinzu und speichere die Unit als UFlaeche gleich ab. In die UFlaeche kommt nun die Klassendeklaration hinein: unit UFlaeche; interface type TFlaeche = class protected function GetFlaeche:double; virtual; abstract; function GetUmfang:double; virtual; abstract; public property Flaeche:double read GetFlaeche; property Umfang:double read GetUmfang; end; implementation end. Erläuterung: Die beiden Methoden (GetFlaeche und GetUmfang) sind als abstrakt definiert, d.h. die Methodenrümpfe werden erst in den abgeleiteten Klassen implementiert. Abstrakte Methoden in Delphi gibt es in zwei Ausführungen, zum einem virtual und zum anderen dynamic sein. Virtuelle Methoden sind geschwindikeitsoptimiert und dynamische Methoden codeoptimiert (gibt weniger Code), sonst gibt's es keine Unterschiede. Die beiden Eigenschaften (Flaeche und Umfang) sind schreibgeschützt und rufen einfach die Funktionen auf, die die Werte berechnen. Hier passiert noch nicht sehr viel. Kommen wir deshalb gleich zu den Erben TRechteck und TKreis. (am besten für jede Klasse eine extra Unit benutzen!): unit URechteck;

interface

uses UFlaeche;

type
  TRechteck=class (TFlaeche)
  private
    FBreite: Currency;
    FLaenge: Currency;
    procedure SetBreite(const Value: Currency);
    procedure SetLaenge(const Value: Currency);
    protected function GetFlaeche: Double; override;
    function GetUmfang: Double; override;
  public
    constructor create (ALaenge, ABreite:Currency);
    property Laenge:Currency read FLaenge write SetLaenge;
    property Breite:Currency read FBreite write SetBreite;
  end;

implementation

{ TRechteck }

constructor TRechteck.create(ALaenge, ABreite: Currency);
begin
  inherited create;
  FBreite:=ABreite;
  FLaenge:=ALaenge;
end;

function TRechteck.GetFlaeche: Double;
begin
  result:=Breite*Laenge;
end;

function TRechteck.GetUmfang: Double;
begin

  result:=2*Breite+2*Laenge;
end;

procedure TRechteck.SetBreite(const Value: Currency);
begin
  FBreite := Value;
end;

procedure TRechteck.SetLaenge(const Value: Currency);
begin
  FLaenge := Value;
end;

end.

Hier sehen wir jetzt endlich mal, die komplette Funktionalität unseres Zieles. Was hier auffällt, ist im Konstruktor / Destruktor der Aufruf inherited create; Mit diesem Aufruf wird der Konstruktor der Elternklasse von TRechteck also TFlaeche aufgerufen. Aber Moment mal, in TRechteck gibt es doch gar keinen Konstruktor create!

TObject - Die Basisklasse von Delphi

In Delphi gibt es die Basisklasse TObject. Von dieser Klasse erben alle anderen Klassen die Methoden, ob gewollt oder ungewollt. TObject sieht so aus: TObject = class constructor Create; procedure Free; class function InitInstance(Instance: Pointer): TObject; procedure CleanupInstance; function ClassType: TClass; class function ClassName: ShortString; class function ClassNameIs(const Name: string): Boolean; class function ClassParent: TClass; class function ClassInfo: Pointer; class function InstanceSize: Longint; class function InheritsFrom(AClass: TClass): Boolean; class function MethodAddress(const Name: ShortString): Pointer; class function MethodName(Address: Pointer): ShortString; function FieldAddress(const Name: ShortString): Pointer; function GetInterface(const IID: TGUID; out Obj): Boolean; class function GetInterfaceEntry(const IID: TGUID): PInterfaceEntry; class function GetInterfaceTable: PInterfaceTable; function SafeCallException(ExceptObject: TObject; ExceptAddr: Pointer): HResult; virtual; procedure AfterConstruction; virtual; procedure BeforeDestruction; virtual; procedure Dispatch(var Message); virtual; procedure DefaultHandler(var Message); virtual; class function NewInstance: TObject; virtual; procedure FreeInstance; virtual; destructor Destroy; virtual; end; Da wird schon eine ganze Menge an Methoden erzeugt. Wo steht jetzt aber in unserem Code, dass von TObject geerbt werden soll? Wird hinter dem Aufruf TFlaeche = class nichts angegeben, dann wird automatisch von TObject geerbt. Letztendlich haben wir dann keine Basisklasse erzeugt, denn unsere Klasse TFlaeche erbt ja schon von TObject. Da dieser Umstand manchmal etwas zur Verwirrung beitragen könnte, ist es sinnvoll grundsätzlich die Elternklasse zu nennen! Kommen wir nun wieder zu unserem Beispiel und zur Klasse TKreis: unit UKreis; interface uses UFlaeche; type TKreis=class(TFlaeche) private FRadius: Currency; procedure SetRadius(const Value: Currency); protected function GetFlaeche: Double; override; function GetUmfang: Double; override; public constructor create (ARadius:Currency); property Radius:Currency read FRadius write SetRadius; end; implementation uses Math; { TKreis } constructor TKreis.create(ARadius: Currency); begin FRadius:=ARadius; end; function TKreis.GetFlaeche: Double; begin result:=SQR(Radius)*Pi; end; function TKreis.GetUmfang: Double; begin result:=2*pi*Radius; end; procedure TKreis.SetRadius(const Value: Currency); begin FRadius := Value; end; end. Achtung! Die Einbindung der Unit Math nicht vergessen, denn dort ist pi definiert! Um wirklich die letzten Zweifler von der Mächtigkeit von OOP zu überzeugen, leiten wir jetzt noch eine Klasse TQuadrat ab. Diesmal aber nicht von TFlaeche sondern von TRechteck: unit UQuadrat; interface uses URechteck; type TQuadrat=class(TRechteck) private protected public constructor create(ABreite:Currency); reintroduce; end; implementation { TQuadrat } constructor TQuadrat.create(ABreite: Currency); begin inherited create (ABreite, ABreite); end; end. Der Konstruktor ruft einfach den Konstruktor der Klasse TRechteck auf und übergibt als Parameter einfach die Seitenlänge des Quadrates zweimal. Mit der Direktive "reintroduce" wird sicher gestellt, dass beim Erzeugen eines Objektes nur der Konstruktur von TQuadrat sichtbar ist und nicht zusätzlich der von TRechteck!

Klassenmethoden und Klassenvariabeln

Als letzten Punkt werde ich den Bereich der Klassenmethoden und -variabeln ansprechen. Vorweg: Klassenvariabeln wie in C++/Java gibt es in Delphi nicht, man kann sich aber relativ einfach behelfen. Klassenmethoden sind Methoden, die in einer Klasse definiert werden und KEIN Objekt brauchen um ausgeführt zu werden! Sie gehören also der Klasse und nicht irgendeinem deklarierten Objekt! Eine solche Klassenvariable kann z.B. dazu benutzt werden um zu zählen wie viele Objekte von dieser Klasse erzeugt wurden. Eine Klassenvariable wird wie folgt erzeugt: class function count:integer; Vor die Methode kommt einfach das Schlüsselwort class. Die Methode wird dann ganz normal implementiert. Hier der komplette Code unserer Klasse TFlaeche: unit UFlaeche; interface type TFLaeche=class private protected function GetFlaeche:double; virtual; abstract; function GetUmfang:double; virtual; abstract; public constructor create; reintroduce; destructor destroy; reintroduce; class function count:integer; property Flaeche:double read GetFlaeche; property Umfang:double read GetUmfang; end; implementation var Anzahl:Integer; { TFLaeche } class function TFLaeche.count: integer; begin result:=Anzahl; end; constructor TFLaeche.create; begin inherited; inc(Anzahl); end; destructor TFLaeche.destroy; begin dec(Anzahl); inherited; end; end. Diesmal hat sogar der Konstruktor und der Destruktor was zu tun. Sie sollen beim Erzeugen / Löschen eines Objektes zur Variable anzahl eins addieren / subtrahieren, sodass dort die Anzahl aller erzeugten Objekte verzeichnet ist. Die Variable anzahl ist eine hausgemachte Klassenvariable, da auf sie nur durch die Klassenfunktion zugegriffen werden kann. Das wars dann auch schon. Klar kann ich auf ein paar Seiten nur die Oberfläche eines Themas ankratzen, zu dem Fachautoren Bücher schreiben. Bücher über OOP und Delphi gibts aber leider kaum. In fast allen Delphi-Büchern steckt etwas OOP drin. Die beiden Literaturtipps sind daher nur bedingt was für Neueinsteiger.