Step 19 - OOP - Einführung

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!

Echte Großzügigkeit gegenüber der Zukunft besteht darin, alles in der Gegenwart zu geben. (Audrey Hepburn)

 

 

Step 19 - OOP - Einführung

Um letztendlich ein eigenes "Objekt" zu erstellen, benötigen wir erstmal etwas Theorie:
Wer sich mit der Delphi-Objekthierachie bereits auskennt, kann den folgenden Abschnitt getrost überspringen.
Objekte sind, wie es Jan bereits in Step 08 beschrieben hat, eine konzentrierte Sammlung von Methoden, Eigenschaften, und sog. Events (Ereignissen), mit welchen wir uns jedoch erstmal nicht befassen werden. Objekte haben im Grunde immer eine Funktion: Die Arbeit beim Programmieren zu vereinfachen, da nur zusammengepackt wird, was zusammen gehört. Hierbei spricht man auch von sog. Kapselung. Die VCL ist die größte "Objekte"-Sammlung in Delphi. Sie umfasst jede Menge "visuelle" Komponenten aber auch "nicht visuelle" Komponenten ( VCL = Visual Component Library). Eine visuelle Komponente ist z.B. ein Button des "Types" TButton. Diesen kann man von der Tool-Palette einfach nehmen und auf einem Formular platzieren, wie man es möchte. Eine "nicht visuelle" ist z.B. der Timer. Dieser ist zur Laufzeit unsichtbar und hat im Grunde nur eine Aufgabe: In einem gegebenen Intervall das "OnTimer-Event" auszuführen. Wer bereits einige VCL-Komponenten getestet hat, ist sicher schonmal in der Abteilung "Zusätzlich" auf den BitBtn ("BitButton") gestoßen.
Der BitBtn ist, wie man im Objektinspektor sehen kann, vom Typ TBitBtn und unterscheidet sich vom "normalen" Button in einigen Eigenschaften, wie z.B. dem Glyph. Hinter dieser Eigenschaft verbirgt sich die Funktion, dass man ein kleines Bitmap auf dem Button anzeigen lassen kann, welches dann wiederum per Eigenschaft Layout positioniert werden kann. Worauf ich hinaus möchte, ist, dass dieser TBitBtn immer noch alle Eigenschaften vom Standard-TButton hat. Dies nennt man Vererbung. Genauer gesagt handelt es sich hierbei um die Vererbung von Klassen. Die Klasse TButton ist dabei die Elternklasse von TBitBtn. Eine Klasse Y kann also von einer anderen Klasse X vererbt werden. Es bekommt dann alle Eigenschaften und Methoden, wie es die Eltern-Klasse hat, sofern die Zugriffe nicht in der Elternklasse eingeschränkt wurden (das kommt erst später genauer, wichtig ist hier erstmal, dass es auch Einschränkungsmöglichkeiten gibt). Diese abgeleiteten Eigenschaften und Methoden können dann erweitert oder modifiziert werden (genaueres siehe später -> OOP für Fortgeschrittene). Klassen können "instanziiert" werden, d.h. dass man eine Instanz einer Klasse - somit das eigentliche "lebende Objekt" - erstellt. Es können bliebig viele Instanzen von einer Klasse erstellt werden - wie eben viele Buttons auf ein Formular eingefügt werden kann.
Beim Instanziieren kommt dann eine weitere wichtige Begrifflichkeit zur Sprache, die ich auch auf später schieben, aber dennoch erwähnen möchte: Die sog. Polymorphie ist die Möglichkeit Objekte von abgeleitete Klassen untereinander auszutauschen und ein bestimmtes Verhalten in Form von Algorithmen gemeinsam verwenden zu können. Dieses auch als "Vielgestaltigkeit" bezeichnete Konzept hilft noch mehr dabei, dass z.B. Sortier-Funktionen nicht für jede Abgeleitete Klasse neu programmiert werden muss, sondern einmal als Algorithmus aufgesetzt, kann dieser dann mit allen Unterklassen aufgerufen werden. Polymorphie war im ursprünglichen Delphi-Lernen.de nicht enthalten, da es ein komplexeres Thema ist bzw. für meine damalige Sichtweise gewesen ist. Da es aber ein wichtiger Grundstein in der OOP ist, wird es im neuen Kapitel zu OOP Fortsetzung Teil 3 bei der Vertiefung neu aufgeführt.
Nun erstmal genug "pure" Theorie - nun etwas Praxis:
Wir starten Delphi und legen eine neue VCL-Formularanwendung an. Klassen werden in Delphi normalerweise in neuen Units erstellt. Also erstellen wir noch zusätzlich eine neue Unit:
Datei -> Neu -> Unit
(oder Delphi-Projekte -> Delphi-Dateien -> Unit)
Nun dürften wir folgendes in der Code-Ansicht (ggf. F12) sehen:
unit Unit2;
interface

implementation

end.
 
Dies sind die Grundbausteine jeder Unit. Wie in Step 05 bereits beschrieben, darf der Name der Unit (hier "Unit2") nicht verändern, wenn man nicht den Dateinamen gleichsam mitverändert. Und Achtung: Groß- und Kleinschreibung ist hierbei wichtig!
Als nächstes sollte ich vielleicht doch mal noch erklären, was wir genau vorhaben: Wir werden eine Klasse erstellen, mit dessen Hilfe wir ganz einfach einen Text mittels der Caesar-Verschlüsselung verschlüsseln können. Als zweiten Schritt habe ich mir gedacht das Objekt zum Vigenére-Verfahren zu vererben. Die Theorie zu den beiden Verschlüsselungs-Verfahren findet man sicher überall im Internet, daher werde ich nicht weiter darauf eingehen.
Nun ist es Lohnenswert, gleich beim Abspeichern sich folgendes anzueignen (was allerdings jedem selbst überlassen ist, wie es genannt werden soll): Wichtig beim Abspeichern ist immer: keine Leer- oder Sonderzeichen eingeben! Nun klicken wir auf das Symbol für alles speichern in der Symbolleiste.
Statt "Unit1" und, da es die Unit des Hauptformulars ist: "MainUnit"
Statt "Unit2": "mCaesar"
Und statt "Projekt1" nennen wir es mal: "OOP1Beispiel".
Nun müssen wir in unserer mCaesar-Unit (das Modul "m"Caesar, welches das Objekt Caesar enthält) noch das Objekt anlegen:
  • zwischen interface und implementation werden wir es wie folgt definieren.
  • Wir geben mit einer Zeile Abstand zu interface und implementation den Bezeichner "type" ein
  • dannach in einer neuen Zeile und etwas eingerückt das Wörtchen "class" und drücken Tab. (Groß- und Kleinschreibung beachten!)
Nun, wenn alles richtig eingegeben wurde, seid ihr Zeugen des Live-Template-Systems von Delphi geworden :-)
Das Ganze sollte nun so aussehen, wobei "MyClass" und "Component" umrahmt sind:
unit mCaesar;

interface

type
  TMyClass = class(TObject)
  private
    { private declarations }
  protected
    { protected declarations }
  public
    { public declarations }
    
  published
    { published declarations }
  end;

implementation

end.
 
An den umrahmten Stellen bittet uns das Live-Template etwas einzugeben, wobei wir beachten sollten, dass wir nur per Tab weiter kommt. Hier geben wir Caesar und Object ein:
  TCaesar = class(TObject)
  private
    { private-Deklarationen }
  protected
    { protected-Deklarationen }
  public
    { public-Deklarationen }
    
    // hier geben wir gleich wieder etwas ein...

  published
    { published-Deklarationen }
  end;
...und nun eine kurze Beschreibung, was die einzelnen Abschnitte bedeuten:
class(TObject)
Hierbei leiten wir die "Urklasse" TObject ab, und erstellen unsere eigene Klasse. Die folgenden Bezeichner dienen zur Verwaltung der Sichtbarkeit von Methoden und s.g. "Feldern" (im Grunde Variablen).
 
privat
Methoden die hier deklariert werden, werden nach dem Instanzieren nicht "gesehen". Hier sollten außer den Methoden alle Felder ( = objekteigene Variablen), beginnend mit einem "f" (für "field") eingetragen werden. Dabei zuerst die Felder, und dann die einzelnen Methoden. (Die Reihenfolge gilt generell)
 
protected
Hier ist das selbe, wie bei privat, nur dass auch bei der Vererbung auf die Methoden und Felder zugegriffen werden kann. Felder und Methoden, die unter private deklariert sind, können somit, wenn man die Klasse vererbt, nicht mehr verwendet werden!
 
public
Hier werden dann letztendlich die Eigenschaften (durch Bezeichner "property" eingeleitet) sowie sichtbare Methoden deklariert.
 
published
Bei der Komponentenentwicklung ist dies wichtig. Hier stehen dann die Eigenschaften und "Events", welche dann im Objektinspektor angezeigt werden.
 
So wieder mal genug Theorie für den Anfang :-)
Weiter im Programm:
Der published-Abschnitt kann getrost gelöscht werden. Um später eine Instanz unserer Klasse "erzeugen" zu können, benötigen wir einen Constructor:
    // ...
  public
    { public-Deklarationen }
    
    constructor Create;
  end;

implementation


// Constructor implementieren:
constructor TCaesar.Create;
begin
  // Constructor von Elternobjekt ausführen:
  inherited Create;
end;


end.
Im Constructor wird alles implementiert, was den direkten Nutzen des Objektes ermöglicht. In unserem Fall brauchen wir eine Liste von Buchstaben, die dann mit der Caesar-Verschlüsselung entsprechend verschlüsselt werden kann. Um die Buchstaben anlegen zu können, brauchen wir ein Feld, das wir im private-Abschnitt ablegen:
  private
    fAlphabet : String;
Die Werte können wir sehr einfach erstellen, indem wir uns die for-Schleife zu Nutzen machen:
constructor TCaesar.Create;
var
  c:Char;
begin
  fAlphabet:='';
  for c:='a' to 'z' do
    fAlphabet:=fAlphabet+c;
Und nun haben wir ganz einfach das kleine Alphabet erstellt.
Im Grunde können wir jetzt schon an die Funktion DeCode gehen, in der letztendlich die Caesar-codierung (& -decodierung) durchgeführt wird: Wir deklarieren unter protected eine function des oben genannten Namens mit den Parametern orig, vom Typ String, sowie move, vom Typ Byte, und einem String als Rückgabewert. Im implementation-Abschnitt implementieren wir den Algorithmus fürs Codieren:
protected
    function DeCode(orig:String; move:Byte):String;

  //...

function TCaesar.DeCode(orig:String; move:Byte):String;
begin
  
end;
Und nun zum Algorithmus:
Es soll geschaut werden, an welcher Stelle ein Buchstabe orig[i] im Alphabet ist, um ihn anschließend um move Schritte zu verschieben. Zur Erinnerung: Ein String ist wie ein Array nutzbar: "Array of Char". Dieses können wir einfach per for-Schleife "durchiterieren" und so jeden Buchstabe für Buchstabe "codieren".
(1) Dies geht ganz automatisch indem wir zwei verschachtelte for-Schleifen verwenden. Die erste for-Schleife zählt den Index des gerade zu verschlüsselnden Chars im Strings hoch. So können wir wirklich jeden Buchstaben für Buchstaben behandeln.
(2) Die zweite (verschachtelte) for-Schleife zählt von 1 bis zur Länge des Alphabetes hoch.
(3) So können wir den Buchstaben des zu codierenden Wortes mit dem des Alphabetes vergleichen.
(4) Wenn der Buchstabe identisch ist, brauchen wir dann "nur noch" um move Schritte verschieben.
Eine weitere Überlegung ist noch wichtig: Was kommt eigentlich, wenn die Anzahl an Schritten größer ist, als das Alphabet lang? Wenn wir z.b: "x" um 5 Schritte verschieben wollen?
x + 1 => "y";
y+2 => "z".
Nun brauchen wir schon bei x+3 => "a" den Index bei 1 gestartet für x+5 kommen wir somit auf "c".
Jeder der schon einmal auf ein Array mit einem zu hohen Index zugreifen wollte, kennt den Fehler "Index out of bounds". Das bekommen wir nur durch eine Berechnung in den Griff: Modulo.
Wir haben Length(fAlphabet) = 26;
Zudem kennen wir den Index von x: index = 24;
Wenn wir 24+5 berechnen bekommen wir index_coded = 29;
Damit wir sozusagen von "vorne" anfangen können, also bei 1, müssen wir die Modulo-Rechnung prüfen 29 mod 26 = 3;
(5) Was ist mit move = 2? Hier ist index_coded = 26; Funktioniert hier diese Modulo-Rechnung?
26 mod 26 = 0;
0 ist allerdings kein Element im String, da dieser erst bei 1 anfängt.
Diesen "Rechenfehler" bekommen wir durch eine einfache Fall-Unterscheidung hin.
Jetzt bleibt noch die Frage: was passiert, wenn der Buchstabe oder das Zeichen nicht im Alphabet ist? Wir halten uns die Frage im Hinterkopf und machen erstmal so weiter. Die Implementierung des DeCode-Algorithmus sieht wie folgt aus:
function DeCode( orig : String; move : Integer) : String;
var
  index, index_coded : Integer;
  coded : String;
begin
  coded := '';
  // (1)
  for index := 1 to Length( orig) do
    // (2)
    for index_coded := 1 to Length( fAlphabet) do
      // (3)
      if orig[ index] = fAlphabet[ index_coded] then
      begin
        // (5)
        if index_coded + move > Length( fAlphabet) then
          // (4) - Teil 1 mit Modulo
          coded := coded + fAlphabet[ (index_coded + move) mod Length( fAlphabet)]
        else
          // (4) - Teil 2
          coded := coded + fAlphabet[ index_coded + move];
      end;
  result := coded;
end;
Nun, testen wir den Algorithmus mal schnell gedanklich mit dem Beispiel-Text "Hallo ziemlich kleines Beispiel!" und verschieben um eine Stelle.
Nach der Erwartung sollte jeder der Buchstaben ein Buchstabe weiter: "Ibmmp ajfnmjdi lmfjoft Cfjtqjfm!";
Ok, nun steigt die Spannung - wird das der Fall sein? Wir implementieren den Test als Test 1.
Um das zu machen, müssen wir lediglich die Unit mCaesar einbinden. Da wir das lediglich in der MainUnit benötigen, genügt die "lokaleste" Einbindung. Das ist im Implementation-Teil:
implementation

uses
  mCaesar;

{$R *.dfm}
Nun können wir einen Button und ein Edit ins Formular einfügen und das TCaesar wie folgt testen:
procedure TForm1.Button1Click( Sender: TObject);
var
  // Deklaration Caesar:
  caesar : TCaesar;
begin
  // instanziieren:
  caesar := TCaesar.Create;
  // Anschließend können wir die "Eingabe" mit in der "Ausgabe" in
  // einer Zeile abbilden:
  Edit1.Text := caesar.DeCode( 'Hallo ziemlich kleines Beispiel', 1);
  // Jedes Objekt, das wir erstellen, müssen wir auch wieder aus dem
  // Speicher entfernen. Dafür gibt es die Methode "Free".
  caesar.Free;
end;
Einmal ausgeführt finden wir das Ergebnis: "bmmpajfnmjdilmfjoftfjtqjfm".
 
Zur Info: Den kompletten Source-Code für dieses Beispiel ist unter GitHub. Um diese Unit von dem kommenden "korrigierten" Quellcode unterscheiden zu können wurde die Unit "mCaesar" in "mCaesarFuerTest1" umbenannt.
 
Was ist jetzt also passiert?
Genau! Die Großbuchstaben sowie andere Zeichen, die nicht verschlüsselt werden (Das Leerzeichen und das Ausrufezeichen) gingen verloren. Damit dies nichtmehr passiert, müssen wir den Algorithmus so verändern, dass zum einen die Großbuchstaben auch verschoben werden, was mittels UpperCase(fAlphabet) möglich ist, und zum Anderen alle übrigen Zeichen gradewegs übernommen werden.
function TCaesar.DeCode( orig : String; move : Integer) : String;
var
  // (1)
  originalChr, codedChr : Char;
  isUpperCase : Boolean;
  index, index_coded : Integer;
  coded : String;
begin
  coded := '';
  for index := 1 to Length( orig) do
  begin
    // (2)
    originalChr := orig[ index];
    isUpperCase := (originalChr = UpperCase(originalChr));
    // (3)
    if isUpperCase then
      originalChr := LowerCase( originalChr)[1];
    // (4)
    codedChr := #0;
    for index_coded := 1 to Length( fAlphabet) do
      // (5)
      if originalChr = fAlphabet[ index_coded] then
      begin
        if index_coded + move > Length( fAlphabet) then
          // (6)
          codedChr := fAlphabet[ (index_coded + move) mod Length( fAlphabet)]
        else
          codedChr := fAlphabet[ index_coded + move];
      end;
    // (7)
    if codedChr <> #0 then
    begin
      // (8)
      if isUpperCase then
        coded := coded + UpperCase( codedChr)
      else
        coded := coded + codedChr;
    end
    else
    begin
      coded := coded + originalChr;
    end;
  end;
  result := coded;
end;
Um den Code erstmal implementieren zu können, brauchen wir 3 weitere Edits, EdOriginal, EdMove, EdCoded und einen Button zum Ausführen des Codes. Die genaue Implementierung werde ich nach der Erklärung zu oberem Sourcecode darstellen.
Bevor wir nun eine Änderung nach der Anderen durchgehen noch ein kurzer Hinweis:

Hinweis

Bei der Implementierung müsstet ihr einen Fehler vom "Live-Debugger" bekommen:
Nicht definierter Bezeichner 'UpperCase'.
In dem Fall suchen wir eben nach der Unit, bzw wir lassen suchen: Rechtsklick drauf, Refactoring ->Unit suchen... und schon kommt ein kleines Fenster, in dem SysUtils.UpperCase als einziger Eintrag steht. Einmal auf OK und schon ist unsere Klasse fertig :-)
zu (1): Wir deklarieren weitere Variablen, die wir als Speicher für die Informationen benötigen um die Großbuchstaben und die Buchstaben, die nicht codiert werden können, verarbeiten zu können. Wir gehen dabei Buchstabe für Buchstabe durch und brauchen dafür den ursprünglichen Buchstaben den wir im Fall, dass er nicht codiert werden kann um ihn einfach wieder zurück zu geben.
zu (2): Zuerst setzen wir den original Buchstabe, der zu codieren ist. Das geht innerhalb der ersten for-Schleife. Hier merken wir uns in isUpperCase, ob es ein Grußbuchstabe war, oder nicht.
zu (3): Falls wir hier bemerken, dass der aktuelle Buchstabe ein Großbuchstabe ist, dann wandeln wir ihn mit LowerCase in einen kleinen Buchstaben um, damit wir ihn mit unserem Alphabet korrekt codieren können.
zu (4): Hier initialisieren wir den Char, der den codierten enthalten wird, mit dem "0-Char". So brauchen wir am Ende (bei 7) nur prüfen, ob dieser weiter 0 ist, oder wirklich ein Char in unserem Alphabet gewesen ist.
zu (5): Statt den orig[index] zu vergleichen, vergleichen wir nun den originalChar, da dieser ja auch die LowerCase-Version des Buchstaben sein kann.
zu (6): Bei der Codierung ändert sich lediglich die Zuweisung - wir nutzen nicht coded um den kompletten String zu füllen, sondern behandeln wirklich nur den einen Char.
zu (7): Nach der möglicherweise durchgeführten Codierung nutzen wir wieder den "0-Char zur Prüfung, ob die Codierung wirklich durchgeführt wurde. Hier ist der Punkt, an dem wir dann genau wissen, ob wir den codierten Buchstaben verwenden können (if-Bedingung ist true), oder ob die Codierung garnicht stattgefunden hat (weiter mit else).
zu (8): Die allerletzte Frage, die nun noch offen ist, ist die Frage, ob wir einen Großbuchstaben codiert hatten, oder nicht. Falls es ein Großbuchstaben war, müssen wir den codierten wieder zurückwandeln, da wir ihn ja bei (3) in einen kleinen umgewandelt hatten. Ansonsten fügen wir den codierten Char dem coded-String an.
 
Beim Nutzen von instanziierten Objekten können wir auch hier ein Live-Template nutzen: "try". Geb dabei beim nächsten Button-Click einfach mal "try" ein und drück einmal die Tab-Taste. Nun siehst du folgende Zeilen:
  MyClass := TObject.Create(Self);
  try
    
  finally
    MyClass.Free;
  end;
MyClass, Component und "(Self)" sind umrandet und können mit Tab "durchgegangen" werden. Dabei können wir "MyClass" in "caesar" und "Component" in "Caesar" ändern und "(Self)" entfernen. Das folgende Code-Beispiel zeigt nun die komplette Implementierung:
procedure TForm1.Button2Click(Sender: TObject);
var
  // Deklaration
  caesar : TCaesar;
  // move soll die Anzahl an Verschiebungen in umgewandler Weise speichern.
  move : Integer;
begin
  caesar := TCaesar.Create;
  try
    // edMove ist das Eingabefeld für die Anzahl an Verschiebungen.
    move := StrToInt( edMove.Text);
    // edOriginal beinhaltet den Text, der codiert werden soll.
    // edCoded wird den codierten Inhalt ausgeben
    edCoded.Text := caesar.DeCode( edOriginal.Text, move);
  finally
    // Und am Ende, das Objekt
    caesar.Free;
  end;
end;
 
Wie bereits erwähnt kann der komplette Source-Code unter GitHub eingesehen werden. Die "fertige" Version ist in mCaesar.pas implementiert.
 
Oben sind wir darauf eingegangen, dass es drei wichtige Punkte in der Objektorientierten Programmierung gibt. Die Kapselung von Informationen sind wir schon durchgegangen. Im kommenden Kapitel werden wir die Caesar-Implementierung durch Vererbung in Vigenére erweitern. Die Polymorphie ist allerdings etwas komplexer, so dass wir hier erst in der Vertiefung weiter einsteigen. Also, los geht's mit der Vererbung im nächsten Kapitel.
Viel Spaß