Trennen Sie Logik und Darstellung

Ein oft gemachter Fehler ist es, Logik und Darstellung zu verweben. Dies geht zum Teil so weit, dass visuelle Controls als Datenspeicher missbraucht werden. Das sieht dann beispielsweise so aus:
procedure TfrmMain.Button1Click(Sender: TObject);
var
  Ergebnis: integer;
begin
  Ergebnis := StrToInt(Label1.Caption) + StrToInt(Label2.Caption);
  Label3.Caption := IntToStr(Ergebnis);
end;
Was ist daran auszusetzen?
Visuelle Controls sind dazu da, Daten darzustellen und nicht dazu, diese zu speichern. Im obigen Beispiel werden nummerische Werte in Labels festgehalten. Daher müssen für die Berechnung und Darstellung des Ergebnisses 3 Datenkonvertierungen vorgenommen werden. Aber auch bei Stringwerten haben Sie das Problem, dass Sie später nicht mehr wissen, welches Label welche Daten bereithält. Dieser Umstand trägt nicht gerade zu einer leichten Wartbarkeit des Codes bei. Sie können dies einfach umgehen, indem Sie entsprechende Datencontainer (sprich: Variablen) deklarieren und Ihre Daten dann über diese verwalten. Die visuellen Komponenten werden dann lediglich zur Darstellung benötigt, damit der Endbenutzer eine optische Rückmeldung erhält.
Nur der Vollständigkeit halber ein Gegenbeispiel für obigen Code:
type
  TfrmMain = class(TForm)
    lblZahl1: TLabel;
    lblZahl2: TLabel;
    lblErgebnis: TLabel;
    Button1: TButton;
    procedure Button1Click(Sender: TObject);
  private
    { Private-Deklarationen }
    FZahl1,
    FZahl2,
    FErgebnis: integer;
    procedure SetErgebnis(const Value: integer);
    procedure SetZahl1(const Value: integer);
    procedure SetZahl2(const Value: integer);
    property Zahl1: integer read FZahl1 write SetZahl1;
    property Zahl2: integer read FZahl2 write SetZahl2;
    property Ergebnis: integer read FErgebnis write SetErgebnis;
  public
    { Public-Deklarationen }
  end;
  
...

procedure TfrmMain.SetErgebnis(const Value: integer);
begin
  FErgebnis := Value;
  lblErgebnis.Caption := IntToStr(FErgebnis);
end;

procedure TfrmMain.SetZahl1(const Value: integer);
begin
  FZahl1 := Value;
  lblZahl1.Caption := IntToStr(FZahl1);
end;

procedure TfrmMain.SetZahl2(const Value: integer);
begin
  FZahl2 := Value;
  lblZahl2.Caption := IntToStr(FZahl2);
end;

procedure TfrmMain.Button1Click(Sender: TObject);
begin
  (* Wir operieren hier jetzt direkt mit den Integer-Eigenschaften. 
     Dadurch, dass wir ihnen einen Setter spendiert haben, werden
     die Labels durch diese automatisch aktualisiert. *)
  Zahl1 := 10;
  Zahl2 := 32;
  Ergebnis := Zahl1 + Zahl2;
end;

Eine andere Stolperfalle lauert darin, Methoden (zu sehr) an bestimmte Controls zu binden.
Beispiel:
Nehmen wir einmal an, Sie haben ein Memo-Control (nennen wir es mmoResult), in dem je Zeile eine Obstsorte dargestellt wird, Leerzeilen sind aber genauso möglich. Nun möchten Sie die Leerzeilen sowie die Obstsorten Kirsche und Banane löschen.
Erste Variante:
procedure TfrmMain.btnCleanUpClick(Sender: TObject);
var
  i: integer;
begin
  mmoResult.Lines.BeginUpdate;
  try
    for i := mmoResult.Lines.Count - 1 downto 0 do
      if (mmoResult.Lines[i] = '') or (mmoResult.Lines[i] = 'Kirsche') or
        (mmoResult.Lines[i] = 'Banane') then
        mmoResult.Lines.Delete(i);
  finally
    mmoResult.Lines.EndUpdate;
  end;
end;
Getestet, funktioniert, Haken dran :)
Nun wird Ihr Programm weiterentwickelt, es kommt ein ähnliches Memo hinzu. Dort wird dieselbe Funktionalität benötigt. Sie könnten nun per Copy & Paste denselben Code für das neue Memo übernehmen und einfach den Namen ändern, aber damit verstoßen Sie gegen das DRY-Prinzip, was wir ja vermeiden möchten.
Also schreiben wir eine Routine, die das Memo sowie die Strings, die zu löschen sind, als Parameter entgegennimmt:
procedure DeleteMatchingLines(mmo: TMemo; StrOfInterest: array of string);
var
  i, j: integer;
begin
  Assert(Assigned(mmo));
  mmo.Lines.BeginUpdate;
  try
    for i := mmo.Lines.Count - 1 downto 0 do
      for j := Low(StrOfInterest) to High(StrOfInterest) do
        if mmo.Lines[i] = StrOfInterest[j] then
          begin
            mmo.Lines.Delete(i);
            break;
          end;
  finally
    mmo.Lines.EndUpdate;
  end;
end;
Das ist schon flexibler. Nun stellen wir fest, dass es ja eigentlich sinnlos war, hier ein Memo zu verwenden, da der Endbenutzer die Daten an dieser Stelle gar nicht ändern können soll. Also ersetzen wir das zweite Memo durch eine Listbox. Problem: damit funktioniert die obige Routine nicht, da diese ja ein Memo erwartet. Wir haben uns also unnötigerweise an ein bestimmtes Control gebunden. Nun könnten wir wieder mit Copy & Paste arbeiten, aber genau das möchten wir ja weiterhin vermeiden, da wir sonst redundaten Code hätten. Wir sollten uns daher überlegen, was wir eigentlich bearbeiten möchten. Es geht ja nicht um ein Memo an sich, sondern um dessen Lines-Eigenschaft, welche vom Typ TStrings ist. Es bietet sich also an, unseren Parameter in TStrings zu ändern.
procedure DeleteMatchingLines(Lines: TStrings; StrOfInterest: array of string);
var
  i, j: integer;
begin
  Assert(Assigned(Lines));
  Lines.BeginUpdate;
  try
    for i := Lines.Count - 1 downto 0 do
      for j := Low(StrOfInterest) to High(StrOfInterest) do
        if Lines[i] = StrOfInterest[j] then
          begin
            Lines.Delete(i);
            break;
          end;
  finally
    Lines.EndUpdate;
  end;
end;
Somit können wir jedwedes TStrings-Objekt übergeben. Diese Routine kann also z.B. mit
  • TMemo.Lines
  • TComboBox.Items
  • TListBox.Items
  • TStringlist
  • TListItem.SubItems
  • usw.
umgehen, ohne dass eine einzige Zeile Code geändert werden müsste.