Step 04 - OOP Zusammenfassung

Spendenaufruf des DRK Ortsvereins Spielberg e.V.

16. April 2023

 

Hallo liebe Besucherin, hallo lieber Besucher!

In einem ganz anderen Kontext möchte ich hier nun bewerben:
Der Verein, für den ich auch einstehe, benötigt Spenden:


Bitte bei der Überweisung den Verwendungszweck beachten!

Der neuste Stand gibt es hier zu sehen!

Geben ist seliger als Nehmen. (Lukas 6,38)

 

 

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:
  1. Wir setzen unsere nicht instanziieren Objekt-variablen auf "nil".
  2. Bevor wir es ordnungsgemäß instanziieren, prüfen wir, ob unsere Objektvariable gerade "nil" ist. Ist es "nil", so instanziieren wir.
  3. 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ß :)