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; |
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.