1. Das Prinzip
Klassen sind von zentraler Bedeutung in der Objektorientierten Programmierung.
Sie sind Baupläne für ein genau definiertes Objekt.
Diese Definitionen dienen dann zur Verallgemeinerung der Problemlösung.
Dies bedeutet, dass wir ein großes Problem in (möglichst durchschaubare, einfachere) Teilprobleme zerlegen.
Ein Chat z.B. stellt ein "großes Problem" dar, welches wir in mehere Kleine zerlegen könnten:
Senden und Empfangen von irgendwelchen Daten, Benutzerverwaltung, Statusverwaltung, Ansicht des Clients ....
Diese Teilprobleme können dann auf ein Maximum durch Teamarbeit optimiert werden und geben somit eine möglichst
hohe Modularität und erhöhen sowohl Wartbarkeit als auch übersichtlichkeit.
2. Aufbau von Klassen
Standardmäßig sollte eine Klasse immer in einer eigenen Unit eingelagert sein.
Wichtig hierbei ist, dass man sich schon zu Beginn an einen gewissen Programmierstil hält.
Der Klassenname sollte mit "T" (für Type) eingeleitet werden.
Eine Unit, die lediglich eine normale Klasse enthällt, fängt für gewöhnlich mit "u" gefolgt vom Klassennamen (ohne "T") an.
Hier einmal ein Beispiel für eine Solche Klasse mit einer privaten Variable und einem dazugehörigen Property,
welches durch Getter und Setter gelesen bzw. gesetzt wird:
unit uClerk; interface type TClerk = class(TObject) private // at first the fields fName : String; // don't forget the leading "f"! fID : Integer; // now the Getter and Setter-Methods function getName:String; procedure setName(AName :String); function getID:Integer; procedure setID(AID :Integer); public // use the Getter and Setter for the properties property Name : String read getName write setName; property ID : Integer read getID write setID; end; implementation // now Implement the Getter- and Setter-Methods: function TClerk.getName:String; begin // here something can be done with fName before it will be returned result := fName; end; procedure TClerk.setName(AName :String); begin // check here if AName consists of two words if pos(' ', AName) > 1 then fName := AName else // only forename or familyname is given fName := '412 - incorrect'; end; // uncommented function TClerk.getID:Integer; begin result := fID end; procedure TClerk.setID(AID :Integer); begin fID := AID; end; end. |
3. Der "Constructor" und "Destructor"
Um eine Klasse "benutzen" zu können, muss diese erst durch ein Objekt instantiiert werden.
Beim Erstellen des Objektes bietet es sich manchmal an, Werte des Objektes bereits jetzt schon zu initialisieren.
Dies geht indem man einen "Constructor Create" im public-Bereich (überladen) deklariert und implementiert.
Dieser kann beispielsweise auch mit Parameter implementiert werden, sodass richtige Werte bereits hier dem Objekt übergeben werden können:
unit uClerk; interface type TClerk = class(TObject) private //... fields first, then Getter and Setter or other methods public //... properties first, then methods, then constructor and destructor constructor Create( AName : String = 'Harold Maier'; AID : Integer = 24576); destructor Destroy; override; // override inherited Create-Method // from TObject end; implementation //... constructor TClerk.Create( AName : String = 'Harold Maier'; AID : Integer = 24576); begin // important: Call the inherited Constructor from the Parent-Class: inherited Create; // for description see later // use the Setter to set the fields setName( AName); setID( AID); end; destructor TClerk.Destroy; begin // if anything is to be freed at the end... // in this example - there are only simple data types ;) end; end. |
Der "destructor Destroy" muss hingegen überladen werde.
Es handelt sich hierbei um eine Methode, welche kurz vor Freigabe des vom Objekt belegten Speichers durch
Aufruf von MyObjekt.Free ausgeführt wird.
Dies kann z.B. nützlich sein, wenn wir in unserem Objekt selbst ein Objekt benutzen, sodass wir durch
fMyOwnObject.Free dessen Speicherplatz freigeben können.
4. Vererbung (inherited)
Dies ist von der Theorie her sehr einfach:
Wir haben eine Ursprungsklasse, welche bestimmte Methoden und Felder bereitstellt und genutzt werden können.
Diese Klasse ist bereits fertig implementiert und läuft fehlerfrei.
Nun Soll diese jedoch funktional erweitert werden, ohne sie selbst zu verändern.
Also erstellen wir eine neue Klasse und Erben einfach die bestehenden Methoden und Eigenschaften.
In unserem Beispiel nehmen wir einen Abteilungsleiter (DepartmentChief), der insgesammt drei neue Attribute
bekommt: fDepartmentName:String, fIsAtWork:Boolean, fSpecialEarnings:Real;
unit uDepartmentChief; interface uses uClerk; type TDepartmentChief = class(TClerk) // inherite from TClerk private // the [new] fields fDepartmentName : String; fIsAtWork : Boolean; fSpecialEarnings : Real; // Getter and Setter function getSpecialEarnings : Real; procedure setSpecialEarnings( ASpecialEarnings: real); public // Property property SpecialEarnings : Real read getSpecialEarnings write setSpecialEarnings; // could be simple functions because no write-flag neccassary function getDepartmentName : String; function isAtWork : Boolean; constructor Create(AName : String = 'Michael Maier'; AID : Integer = 1; ADepartmentName : String = 'Informatik'; AIsAtWork: Boolean = false; ASpecialEarnings : Real = 0 ); end; implementation // ... here the implementation of the other methods! constructor TDepartmentChief.Create(AName : String = 'Michael Maier'; AID : Integer = 1; ADepartmentName : String = 'Informatik'; AIsAtWork: Boolean = false; ASpecialEarnings : Real = 0 ); begin // important: now with parameters! inherited Create(AName, AID); fDepartmentName := ADepartmentName; fIsAtWork := AIsAtWork; fSpecialEarnings := ASpecialEarnings; end; end. |
5. Überladen von Methoden (overload)
Überladen von Methoden bedeutet im Grunde nur, dass eine gleichnamige Methode mit anderen Parametern nochmal
deklariert und implementiert wird.
Bei unserem Beispiel werden wir den Constructor überladen:
//... uses uClerk, Classes; type TDepartmentChief = class(TClerk) //... constructor Create(AName : String = 'Michael Maier'; AID : Integer = 1; ADepartmentName : String = 'Informatik'; AIsAtWork: Boolean = false; ASpecialEarnings : Real = 0 ); overload; constructor Create(AFileName : String; ALine : Integer); overload; constructor Create(AStringList : TStringList; ALine : Integer); overload; end; implementation // ... constructor TDepartmentChief.Create(AFileName : String; ALine : Integer); begin // Load File and read Line ALine and split into needed parts // and don't forget: inherited create; end; constructor TDepartmentChief.Create(AStringList : TStringList; ALine : Integer); begin // read Line ALine from AStringList and split into needed parts. // and don't forget: inherited create; end; end. |
6. Überschreiben von Methoden (override)
Überschreiben hat besonders beim Vererben eine wichtige Rolle:
Wenn in der Elternklasse eine Methode etwas unsicher implementiert hat, so kann dies nun komplett überschrieben werden.
D.h. die komplette Methode wird neu implementiert.
Dies funktioniert allerdings nur bei Methoden, welche nur in der neuen Klasse verwendet werden.
Wird die urspüngliche Methode X() der Elternklasse in einer anderen Methode Y() in der Elternklasse verwendet,
welche nicht überschrieben wird, so wird in Y() die Methode X() und nicht die überschriebene Methode X()'
verwendet. Überladen ist hier folglich sinnlos.
// in Parent-Class: procedure myOverwrittenMethod(myParam : String); // ... procedure myOverwrittenMethod(myParam : String); begin DoSomeThing; end; // in Child-Class: procedure myOverwrittenMethod(myParam : String); override; //... procedure myOverwrittenMethod(myParam : String); begin DoSomeThingElse; end; |
Da das doch etwas kryptisch vorkommen kann, folgt nun ein einfacheres Beispiel. Hier kurz, die Implementierung
in der Parent-Klasse TClerk.
// uClerk.pas: interface // ... public // ... function getRankTitle:String; // ... implementation // ... function TClerk.getRankTitle: String; begin result := 'Clerk'; end; |
Die Methode soll nun in der TDepartmentChief-Klasse überschrieben werden:
// uDepartmentChief.pas: interface // ... public // ... // Beispiel-Methode in Child-Klasse function getRankTitle: String; // ... implementation // ... function TDepartmentChief.getRankTitle: String; begin result := 'Manager'; end; |
7. Instantiieren der Klasse.
Um unsere Test-Objekte auch mal zu benutzen, verwenden wir ein neues Projekt. Benennen es "Example204" usw. .
Setzen in den Uses unsere zwei Units ein.
Deklarieren im privat-Abschnitt der TMainform-Klasse zwei Felder:
fMyClerk: TClerk und fMyDepartmentChief : TDepartmentChief;
Nun benötigen wir 4 Buttons.
Der Erste erstellt den Clerk, der Zweite gibt den Speicher wieder frei, der Dritte und Vierte macht das gleiche mit fMyDepartmentChief.
procedure TMainForm.BtnCreateClerkClick(Sender: TObject); begin fMyClerk := TClerk.Create; // Parameter optional end; procedure TMainForm.BtnFreeClerkClick(Sender: TObject); begin fMyClerk.Free; end; |
8. Über die Sicherheit.
Wenn Ihr das ganze mal testet, dann bekommt ihr mit Sicherheit den folgenden Fehler präsentiert:
"Ungültige Zeigeroperation.".
Dies beruht darauf, dass Ihr Speicher eines Objektes, welches garnicht existiert, (nochmals) freigeben wollt.
Abhilfe hierbei schafft folgendes Konzept:
- Wir setzen unsere nicht instanziieren Objekt-variablen auf "nil".
- Bevor wir es ordnungsgemäß instanziieren, prüfen wir, ob unsere Objektvariable gerade "nil" ist. Ist es "nil", so instanziieren wir.
- Bevor wir unser möglicherweise instanziiertes Objekt wieder freigeben prüfen wir das gleiche. Ist es nicht "nil", dann befreien wir den Speicher und setzen es auf "nil".
Hierdurch haben wir die maximale Fehlersicherheit in unserem Programm, ohne jetzt auf die weitere
Implementierung einzugehen.
Die Sources für alle Beispiele sind unter
Github
zum Download bereit!
Viel Spaß :)
Viel Spaß :)