Step 14 - Methoden und OOP

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!

Wer anderen hilft, hilft sich selbst. (Konfuzius)

 

 

Step 14 - Methoden und OOP

Mit Methoden meint man in Delphi Funktionen und Prozeduren. Diese Methoden haben die Aufgabe, den Quelltext kürzer und übersichtlicher zu implementieren. Wiederkehrender Code wird damit als Baustein zusammengefasst und der gesammte Code wird kürzer, was auch die Zeit bei Wartungsarbeiten verringern kann. Man gibt diesen Methoden Namen und kann sie dann im späteren Verlauf immer wieder aufrufen.
Prozedur:
procedure [Name]([Parameter:Typ]);
{ Nach dem Schlüsselwort procedure muss man einen beliebigen
Namen angeben, danach kann man in der Klammer die Parameter
angeben oder komplett weglassen (auch die klammern sind irrelevant)}


{hier deklariert man Variablen und Konstanten}
begin
  // Anweisungen
end;
 
Funktion:
function [Name]([Parameter: Typ]): [Rückgabewerttyp];
{ Das gleiche wie bei einer Prozedur, nur muss man noch den Rückgabewerttyp
bestimmen (String, Integer ...)}

{hier deklariert man Variablen und Konstanten}
begin
  // Anweisungen
  result:= // Rückgabewert
end;
 
Als einfaches Beispiel können die folgenden zwei Methoden dienen, die von einer Button-Click gestartet werden. Wichtig dabei ist die Reihenfolge: Die Procedure und Function müssen immer oberhalb des Aufrufenden zu finden sein - also genau in der Reihenfolge des Beispiels.
function getAddition(zahl1 : Integer; zahl2: Integer) : Integer;
begin
  result := zahl1 + zahl2;
end;

function getSubtraktion(zahl1 : Integer; zahl2: Integer) : Integer;
begin
  result := zahl1 - zahl2;
end;


procedure berechneBsp1( addiere : Boolean; zahl1 : Integer; zahl2: Integer);
var
  ergebnis : Integer;
begin
  if addiere then
    ergebnis := getAddition( zahl1, zahl2)
  else
    ergebnis := getSubtraktion( zahl1, zahl2);

  ShowMessage( 'Ergebnis: ' + IntToStr( ergebnis) );
end;


// Das ist die OnClick-Procedure - direkt bei Doppelklick im Formular
// Je nach dem, wie du deine Elemente benannt hast, kann es sein, dass die folgende Zeile
// bei dir "TForm1.Button1Click(..." lautet.
procedure TMainForm.BtnBeispiel1Click(Sender: TObject);
begin
  berechneBsp1(true, 13, 17);
  berechneBsp1(false, 17, 13);
end;
 
Und nun: Ok, das war ein sehr einfaches Beispiel einer Procedure und zwei Funktionen. Wir dürfen hier aber eine besondere Lektion lernen: Diese Implementierung ist so nicht in Ordnung. Um "aus Fehlern zu lernen" hier die Lektion dazu: Was hier nicht beachtet wurde, ist das sog. EVA-Prinzip!

Das EVA-Prinzip

EVA steht für "Eingabe Verarbeitung Ausgabe" und ist prinzipiell das Gesetz der Computer überhaupt. Erst wenn durch irgendjemand oder etwas eine Eingabe ausgelöst wurde, startet eine Verarbeitung, die am Ende mit einer Ausgabe bzw. einem Resultat beendet wird. Im engeren Sinne ist das sogar DAS Prinzip was hinter Funktionen liegt. Die Parameter zahl1 und zahl2 bilden die Eingabe. Zwischen Begin und End findet die Verarbeitung statt und der Result-Wert (durch ":Integer" hinter bei der Funktionsdeklaration beschrieben) ist die Ausgabe. Das EVA-Prinzip sagt auch aus, dass bestimmte Aufgaben getrennt voneinander sein sollten. Somit verstößt die Procedure berechne(..) hier dagegen, da sie nicht nur berechnet, sondern auch eine Ausgabe implementiert. Hier nun kurz die Korrektur - die Funktionen von oben sind weiter gültig, berechneBsp2 ist dabei neu:
function berechneBsp2( addiere : Boolean; zahl1 : Integer; zahl2: Integer) : Integer;
var
  ergebnis : Integer;
begin
  if addiere then
    ergebnis := getAddition( zahl1, zahl2)
  else
    ergebnis := getSubtraktion( zahl1, zahl2);

  result := ergebnis;
end;



procedure TForm1.Button2Click(Sender: TObject);
var
  eingabeZahl1, eingabeZahl2 : Integer;
  ergebnisText : String;
begin
  // Eingabe
  eingabeZahl1 := 17;
  eingabeZahl2 := 13;

  // Verarbeitung
  ergebnisText := 'Ergebnis von Beispiel 1: ';
  ergebnisText := ergebnisText + IntToStr( berechneBsp2(true, eingabeZahl1, eingabeZahl2));
  ergebnisText := ergebnisText + sLineBreak;   // System-Konstante des Zeilenumbruchs
  ergebnisText := ergebnisText + 'Ergebnis von Beispiel 2: ';
  ergebnisText := ergebnisText + IntToStr( berechneBsp2(false, eingabeZahl1, eingabeZahl2));

  // Ausbage
  ShowMessage( ergebnisText);
end;
 
"Call by Value" vs. "Call by Reference"
Lassen wir es etwas spannend: Was passiert bei folgender Funktion? Was zeigt int2 bei der Ausgabe als Wert? 0 oder 10?
function getBothValA( i : Integer) : Integer;
begin
  i := 10;
  result := 20;
end;


// Aufruf weiter unten im Code in einer Button-Click-Procedure:
var
  int1, int2 : Integer;
begin
  int2 := 0;
  int1 := getBothVal(int2);
  ShowMessage( IntToStr( int1));
  ShowMessage( IntToStr( int2));
end;

Ok, wer hier selber versuchen will, darf das - und wer nicht, oder wer schon hat, kann nun weiterlesen:

Wer schon "weiter" geschaut hat, kennt den Unterschied von "getBothValA" zu "getBothValB". Der in getBothValA definierte Aufruf nennt man Call by Value. Es wird lediglich der Wert von int1 auf i übergeben. Damit kann i in der Methode so verwendet werden, wie eine normale lokale Variable. Anders ist durch den var-Bezeichner vor dem Parameter definiert, dass es sich hier um das sog. Call by Reference handelt. In der OOP wird das sehr oft angewendet, da hierdurch auch "Platz" gespart und "Performance" optimiert werden kann. Hier geht es darum, dass nicht nur der Wert übergeben wird, sondern eine Information, wo im Speicher sich das "Objekt" befindet. Alle weiteren Aktionen auf den Parameter werden direkt auf das übertragene Objekt angewendet. In unserem Beispiel ist i := 10 also gleich"bedeutend" wie int1 := 10. Soweit zur Theorie.
 
Verschachtelung
Was auch möglich ist, ist eine Art Verschachtelung von Methoden. Dabei kommt die Methode einfach direkt in den Bereich, in dem sonst Variablen deklariert werden.
Das folgende etwas größere Beispiel stammt aus einem meiner vergangenen Projekte und kann kurz darlegen, warum es übersichtlicher sein kann, Wiederholungen zu kapseln.
// Die "ColorTags"-Methode kann auch anders implementiert werden
//                                mehr erfährst du weiter unten.
// Hier genügt uns, dass wir ein Button-Click-Ereignis erstellt haben,
// daher der Methoden-Name btnColorTagsClick:
procedure TMainForm.btnColorTagsClick( Sender: TObject);
var
  // hier die Definition der lokalen Variablen
  tagBeginSymbol, tagEndSymbol: Char; Col: TColor;
  sourceCode      : String;
  startPosition,
  endPosition,
  lastPosition,
  lengthOfText    : Integer;

  // hier die Definition der lokalen Methoden
  procedure PaintIt( start, leng : Integer; Color : TColor);
  begin
    ReText.SelStart            := start;
    ReText.SelLength           := leng;
    ReText.SelAttributes.Color := Color;
  end;

  // Da PaintIt nur in dieser Procedure (und das 3 mal) aufgerufen wird, und da
  // man beim Programmieren immer so lokal wie möglich programmieren sollte,
  // sollte diese Procedure auch nur hier stehen.

  // Grundsatz: So global wie nötig, so lokal wie möglich!

begin
  // hier initialisieren wir mal ganz einfach die wichtigen Zeichen:
  tagBeginSymbol := '<';
  tagEndSymbol := '>';
  Col := clRed;
  // ReText ist ein RichEdit-Element. Dieses und ein Button genügen für
  // dieses Beispiel als Test der späteren "ColorTags"-Funktion ;)
  sourceCode := ReText.Text;
  // Zeichenumbrüche können hier gemein werden - sie werden "doppelt" gezählt
  // da sie ein Zusammenschluss von Zwei zeichen sind, aber nur als ein Zeichen
  // dargestellt werden - hier ein kleiner Fix für die weitere Behandlung:
  if ReText.Lines.LineBreak = AnsiString(#13#10) then
    sourceCode := stringreplace( sourceCode, ReText.Lines.LineBreak, '§',
                                 [rfReplaceAll, rfIgnoreCase]);

  lastPosition := 0;
  repeat
    lengthOfText  := Length( sourceCode );

    startPosition := pos( tagBeginSymbol, sourceCode);
    endPosition   := pos( tagEndSymbol, sourceCode);

    // Kein weiterer Tag in Sicht: Ende der Bearbeitung durch Abbruch:
    if (startPosition < 1) or (endPosition < 1) then
    begin
      break; // repeat..until verlassen
    end;

    // Start ist nach dem Ende?! => Abbruch durchführen, da ist was "komisch"...
    if (startPosition > endPosition) then
    begin
      break;
    end;

    // Normaler "Tag" entdeckt:
    if startPosition > 0 then
      PaintIt( lastPosition + startPosition - 1,
               endPosition - startPosition+1,
               Col); // Diese Art der Schreibweise kann hilfreich sein
                     // wenn viele Parameter übergeben werden müssen.
                     // Generell kann man sich hier die Frage stellen:
                     // Ist es wirklich notwendig so "viele" Parameter
                     // übergeben zu müssen?
                     // Näheres hierzu kommt in OOP später ;)

    sourceCode := copy( sourceCode,
                        endPosition + Length(tagEndSymbol),
                        lengthOfText);
    lastPosition := lastPosition + endPosition;

    // Falls die Methode lange läuft, sorgt folgende Zeile dafür,
    // dass die Anwendung nicht "einfriert".
    Application.ProcessMessages;
    // Es genügt, die Zeile "sourceCode := copy..." auszukomentieren um
    // Application.ProcessMessages in Aktion zu sehen.
    // Wer App... auch noch auskommentiert, sieht was "einfrieren" bedeutet :)

  until (startPosition<1) or (startPosition=endPosition);
end;
 

Deklaration und Implementierung von Methoden - Erster Einblick in OOP - das "Geheimnisprinzip".

Als Methoden bezeichnet man Prozeduren und Funktionen, welche zu einer Klasse gehören. Dies sind die Stellen bei denen die Funktionen implementiert werden dürfen:
type
  TMainForm = class(TForm)
    BtnDoSmth: TButton;
  private
    { Private-Deklarationen }
    // diese Procedure kann nur in dieser Unit per
    //        MainForm.myPrivatProcedure1 aufgerufen werden.
    procedure myPrivatProcedure1;
  public
    { Public-Deklarationen }
    // diese Procedure kann auch in anderen Units per
    //       MainForm.myPublicProcedure1 aufgerufen werden.
    procedure myPublicProcedure1;
  end;

var
  MainForm:TMainForm;
  // diese Procedure kann auch in anderen Units per
  //     myPublicProcedure2 aufgerufen werden.
  procedure myPublicProcedure2;

implementation

{$R *.dfm}

// Diese Procedure kann nur von allen unten stehenden Methoden aufgerufen werden
procedure myProc(myParam:Integer);
begin
  MainForm.myPrivatProcedure1;
end;

// Ganz wichtig bei Methoden, die in der TMainForm-Klasse deklariert wurden:
// Sie fangen mit TMainForm. an!
procedure TMainForm.myPrivatProcedure1;

  // diese Procedure ist nur innerhalb
  //     der myPrivatProcedure1-Procedure gültig!
  Procedure myPrivatProcedure2;
  begin
    // diese kann auch auf Objekte der Klasse TMainForm zugreifen
    BtnDoSmth.Caption := 'InnerProc';
  end;

begin
  myPrivatProcedure2;
end;


procedure TMainForm.myPublicProcedure1;
begin
  // Hier kann auf alle Objekte, Methoden und Attribute
  //      der Klasse TMainForm zugegriffen werden.
  BtnDoSmth.Caption := 'Public1';
end;


procedure myPublicProcedure2;
begin
  // hier kann nur auf die globalen Objekte der Klasse TMainForm zugegriffen werden.
  MainForm.BtnDoSmth.Caption := 'Public2';
end;

end.
 
Weitere Beispiele:
function isInRange(nmb:Integer):Boolean
begin
  if (nmb>2) and (nmb<20) then
    result := true  
  else
    result := false;
end;

// oder:

function isInRange(nmb:Integer):Boolean
begin
  result := (nmb>2) and (nmb<20);
end;


  //... Aufruf:
  if isInRange(9) then
  // ...


  // MessageBox ist eine oft verwendete Meldung - unbedingt merken!
procedure ErrorMSG(msg:String);
begin
  Application.MessageBox(PChar(msg),'Error!',MB_OK+MB_ICONERROR);
end;
  
  //... Aufruf:
  ErrorMSG('Fehler bei der Kommunikation mit dem Drucker!');
  //...
 
type
  TMainForm = class(TForm)
        // ...
  private
    { Private-Deklarationen }
    procedure TMainForm.ColorTags( reTextField : TRichEdit; tagBeginSymbol,
      tagEndSymbol: Char; Col: TColor);
  public
    { Public-Deklarationen }
        // ...

// Aufruf der Funktion: Direkt in der ButtonClick-Funktion:
procedure TMainForm.BtnColorTags2Click(Sender: TObject);
begin
  ColorTags(ReText_Bsp2, '<', '<''>', clBlue);
end;


// Und hier die als private-Deklarierte Methode.
// Ein wichtiger Unterschied: Sie muss nicht mehr über der
// aufrufenden Funktion deklariert sein, sondern es geht auch darunter.
procedure TMainForm.ColorTags( reTextField : TRichEdit; tagBeginSymbol,
  tagEndSymbol: Char; Col: TColor);
var
  sourceCode      : String;
  startPosition,
  endPosition,
  lastPosition,
  lengthOfText    : Integer;

  procedure PaintIt( start, leng : Integer; Color : TColor);
  begin
    reTextField.SelStart            := start;
    reTextField.SelLength           := leng;
    reTextField.SelAttributes.Color := Color;
  end;

begin
  sourceCode := reTextField.Text;
  // Zeichenumbrüche können hier gemein werden - sie werden "doppelt" gezählt
  // da sie ein Zusammenschluss von Zwei zeichen sind, aber nur als ein Zeichen
  // dargestellt werden - hier ein kleiner Fix für die weitere Behandlung:
  if reTextField.Lines.LineBreak = AnsiString(#13#10) then
    sourceCode := stringreplace( sourceCode, reTextField.Lines.LineBreak, '§',
                                 [rfReplaceAll, rfIgnoreCase]);

  lastPosition := 0;
  repeat
    lengthOfText  := Length( sourceCode );

    startPosition := pos( tagBeginSymbol, sourceCode);
    endPosition   := pos( tagEndSymbol, sourceCode);

    // Kein weiterer Tag in Sicht: Ende der Bearbeitung durch Abbruch:
    if (startPosition < 1) or (endPosition < 1) then
    begin
      break; // repeat..until verlassen
    end;

    // Start ist nach dem Ende?! => Abbruch durchführen, da ist was "komisch"...
    if (startPosition > endPosition) then
    begin
      break;
    end;

    // Normaler "Tag" entdeckt:
    if startPosition > 0 then
      PaintIt( lastPosition + startPosition - 1,
               endPosition - startPosition+1,
               Col);

    sourceCode := copy( sourceCode,
                        endPosition + Length(tagEndSymbol),
                        lengthOfText);
    lastPosition := lastPosition + endPosition;

    // Falls die Methode lange läuft, sorgt folgende Zeile dafür,
    // dass die Anwendung nicht "einfriert".
    Application.ProcessMessages;
    // Es genügt, die Zeile "sourceCode := copy..." auszukomentieren um
    // Application.ProcessMessages in Aktion zu sehen.
    // Wer App... auch noch auskommentiert, sieht was "einfrieren" bedeutet :)

  until (startPosition<1) or (startPosition=endPosition);
end;
        
 
Wer sich hier eine kleine weitere Erklärung wünscht, darf mir gerne via SocialMedia oder Mail schreiben.
 
2007 habe ich erneut eine Runde in der Delphi-AG verbracht und zum Thema Methoden eine Präsentation gehalten. Unter Github ist diese unter "Präsentation zu Methiden" im PDF-Format herunterladbar.
 

Hinweis für "echte Projekte"

Wenn man echte Projekte starten möchte, sollte man bei den Bezeichnungen aufpassen. Zum Beispiel sollte das Hauptformular nicht Form1, sondern besser aussagend z.B. MainForm bezeichnet werden, falls es sich um das Hauptformular handelt.
Genauso dann auch die Unit: MainUnit.
Module, welche Methoden beinhalten, die in irgendeiner anderen Unit genutzt werden, fangen dann mit einem "m" an und es sollen sprechende Namen und bekannte Abkürzungen benutzt werden.
Eine Funktion f(myParam1, myParam2:Integer):Integer; sagt zum Beispiel überhaupt nichts über seinen Nutzen aus, was bedeutet, dass man später bei einem Fehler die ganze Funktion nochmals durchgehen muss.
Ein Sinn hinter den Funktionen geht dabei aber verloren: Eine Funktion soll einen kleinen Baustein darstellen, der immer wieder verwendet werden kann und dessen Namen kurz wieder gibt, was dessen Inhalt tut.

"Echte Projekte"?! Tipps für mehr Tiefgang

Wer bereits hier etwas mehr in die Theorie abbiegen möchte - dem kann ich wärmstens Clean-Code-Developer.de und die Bücher / Werke / Videos von, ich nenne ihn mal, "Gründervater" Robert C. Martin alias Uncle Bob empfehlen.
Viel Spaß!