Codedesign


Ziel dieses Artikels

In diesem Artikel geht es mir darum aufzuzeigen, was guten Code und was schlechten Code ausmacht. Und zwar kommt es mir dabei nicht auf den Algorithmus oder die Codeoptimierung an, sondern auf das Codedesign, also wie das Problem letztendlich im Quellcode gelöst wurde.
Die Idee zu diesem Artikel kam mir, als mir jemand Code gegeben hat, den ich mir mal angucken sollte. Und dieser Code war vom Codedesign, meiner Meinung nach, schlecht.
Was unterscheidet guten Code von schlechten Code? Nun diese Frage ist nicht ganz so einfach zu beantworten, da es teilweise auch Geschmackssache ist und auch eigene Gewohnheiten mit rein spielen. Als aller erstes empfehle ich eine zumindest einheitliche Codeformatierung einzuhalten. Dies erhöht nicht nur die Übersichtlichkeit, sondern auch die Lesbarkeit und die Wartung des Codes durch andere. Als allgemeine Richtlinie sei an dieser Stelle auf den Object Pascal Style Guide von Charles Calvert hingewiesen. Ich betone noch mal, dass es sich dabei nur um eine Empfehlung handelt. Letztendlich ist es reine Geschmackssache und firmeninterne Style Guides gibt es ja auch noch. Nichts desto trotz hat er seine Berechtigung. Hält man sich selbst dran und bekommt Code von anderen, die sich auch daran orientieren, fällt es gleich leichter den Code zu lesen.
"Gutes Codedesign berücksichtigt immer Erfahrungen von anderen Programmierern, und es ist ein Mix aus technischen Gegebenheiten des Compilers/Source Editierung/der Sprache und aus philosophischen Erwägungen wie Wartbarkeit/OOP/Algorithmen/Lesbarkeit/Dokumentation." (Zitat Hagen Reddmann)

Was ist gemeint

Kommen wir nun zum Codedesign. Was verstehe ich unter "Codedesign"? Unter Codedesign verstehe ich die Art und Weise wie man seinen Code aufbaut, strukturiert, anlegt. Welche Kriterien muss nun gutes Codedesign aufweisen? Ihr mal in loser Reihenfolge und ohne Gewichtung:

  1. Trennung von Aufgabe / Funktion und der Benutzerschnittstelle, erleichtert die Wiederverwendbarkeit von Codeteilen.
  2. Aufteilung in Teilprobleme.
  3. Aufteilung in sinnvolle Abschnitte (Subroutinen).
  4. Für sich sprechende Variablen- und Funktionsnamen
  5. So wenig Kommentare wie möglich, so viele Kommentare wie nötig.
  6. So wenig globale Variablen wie möglich.
  7. Subroutinen sollten auf eine Bildschirmseite passen.

Trennung von Code und Oberfläche

Trennt man den eigentlichen Code des Programms von der Benutzerschnittstelle, hat man drei Dinge erreicht:

Aufteilung in Teilprobleme

Teilt man weiterhin das Problem in Teilabschnitte auf, was konsequenterweise dazu führt dass diese Teilprobleme in separaten Subroutinen untergebracht werden, erhöht man auch noch mal die Übersichtlichkeit und Wartbarkeit des Codes. So kann man nämlich für die Subroutinen "sprechenden" Namen vergeben und so die Lesbarkeit erhöhen und Kommentare sparen. Zu diesen Punkt kann man auch noch die Vergabe von sinnvollen Variablennamen zählen. Variablennamen wie x, y, z sind in den seltensten Fällen sinnvoll. Man sollte schon am Namen erkennen, was sie beinhalten. In C/C++ wird auch heute noch gerne die Ungarische Notation eingesetzt. Bei der Ungarischen Notation bezieht man den den Datentyp als Kürzel mit in den Variablennamen ein. Zum Beispiel: dwNumber für eine DWORD Variable. Oder fStatus für eine bool'sche Variable (f = Flag) usw. Ich halte solch eine Variablennamenvergabe für weitgehen überflüssig. Da sie zum einen Variablennamen nur unnötig verlängert und unleserlich macht und zu dem bei modernen Compilern und Programmiersprachen, die typensicher sind, einfach überflüssig ist. In C/C++ wo man praktisch jedem Datentyp alles zuweisen kann, mag sie noch eine Daseinsberechtigung haben. Auf der anderen Seite benutze ich sie aber selber bei der Benennung von Controllelementen auf dem Fenster.

Kommentare

Was die Kommentare angeht, sollte man so viele wie nötig machen, aber so wenig wie möglich. Aus dem Grund, weil auch Kommentare den Lesefluss beim Studieren des Quellcodes stören. Am besten ist meist noch sich selbst kommentierender Code. Wie man das erreicht, werden wir gleich an einem Beispiel sehen.

Globale Variablen

Zum Thema "globale Variablen". Erstmal widersprechen sie dem Konzept der Objekt Orientierten Programmierung. Zum Zweiten erschweren sie eine Fehlersuche, da man bei größeren Projekten, verteilt über mehrere Dateien, leicht den Überblick verliert, wo, von wem, wie eine globale Variable verändert wird. Natürlich gibt es Situationen, in denen es sich nicht vermeiden lässt, wie wir im Beispiel auch gleich noch sehen werden.

Länge und Umfang von Subroutinen

Über den letzten Punkt kann man geteilter Meinung sein. Was eigentlich schon damit anfängt, was eine Bildschirmseite denn ist. Bei einer Auflösung von 1280 x 1024 bekomme ich mehr auf eine Bildschirmseite als bei einer Auflösung von 800 x 600. Aber wie auch immer wir eine Bildschirmseite definieren, bin ich der Meinung, dass es das Lesen und Verstehen eines Quellcodes unheimlich erleichtert, wenn man die gesamte Subroutine mit einem Blick erfassen kann ohne großartig scrollen zu müssen. Hier zu kann man auch noch folgenden Punkt zu zählen: Zeilen sollten nicht länger als 80 Zeichen werden. Einmal, um horizontales Scrollen zu vermeiden, was erwiesenermaßen noch unbeliebter ist als vertikales, und zum Zweiten werden so meist, bei vernünftig gewählter Schriftgröße, hässliche Zeilenumbrüche beim Drucken vermieden.

Ein Beispiel und wie man es besser macht

Kommen wir nun zu einem Beispiel. Folgender Code wurde mir zugesandt. Seine Aufgabe, Umrechnung eines Datums des gregorianischen Kalenders in ein Julianisches Datum. Dabei wird der Tag, der Monat und das Jahr eingegeben und das Programm rechnet dann das Julianische Datum dazu aus. Hier nun der Beispielcode, wie ich ihn bekam:

procedure TForm1.btnBerechnenClick(Sender: TObject);
var
  iTag, iMonat, iJahr: Extended;
  iTemp1, iTemp2: Extended;
  iMonate, iJahre: Extended;
begin
  if Length(edtTag.Text) <> 0 then
  begin
    if Length(edtMonat.Text) <> 0 then
    begin
      if Length(edtJahr.Text) <> 0 then
      begin
        iTag := StrToFloat(edtTag.Text);
        iMonat := StrToFloat(edtMonat.Text);
        iJahr := StrToFloat(edtJahr.Text);

        if iMonat < 3 then
          iJahre := iJahr - 1
        else
          iJahre := iJahr;
        if iMonat < 3 then
          iMonate := iMonat + 12
        else
          iMonate := iMonat;

        iTemp1 := iJahre / 100;
        iTemp2 := 2 - iTemp1 + (iTemp1 / 4);

        edtErgebniss.Text := FloatToStr((365.25 * (iJahre + 4716)) +
                             (30.6001 * (iMonate + 1)) +
                             iTag + iTemp2 - 1524.5);
      end //if Jahr
      else
        ShowMessage('Bitte das Jahr angeben!');
    end //if Monat
    else
      ShowMessage('Bitte den Monat angeben!');
  end //if Tag
  else
    ShowMessage('Bitte den Tag angeben!');
end;

Meine Kritikpunkte:

  1. Die Funktion des Programms ist mit der Oberfläche verwoben. Man sieht kaum, wo eigentlich die Umrechnung statt findet. Als Folge kann man sehr deutlich sehen, wie problematisch es für den Programmierer war die Eingaben zu validieren – und es auch nicht richtig geschafft hat. Die if-Bedingungen machen den Quellcode nicht unbedingt leserlicher.
  2. Daran schließt sich mein nächster Kritikpunkt an, der zwar nichts mit Codedesign zu tun hat, aber nichts desto trotz Erwähnung finden sollte: Bei fehlenden Eingaben wird dies dem Benutzer mittels einer Messagebox mitgeteilt - etwas unschön. Ich fühle mich dann immer etwas vor den Kopf gestoßen und muss die Messagebox erst mal wegklicken. Besser, erst gar keine ungültigen Eingaben zu zulassen, bzw. die Berechnung erst dann zulassen, wenn gültige Eingaben gemacht wurden.
  3. Benennung der Variablen. Was soll das "i"? Für Integer kann es nicht stehen, der Datentyp ist ja Extended. Die halbherzig angewandte Ungarische Notation stiftet hier eigentlich nur Verwirrung.

Wie kann man es nun besser machen? Als allererstes habe ich die Aufgabe des Programms von der Oberfläche getrennt:

////////////////////////////////////////////////////////////////////////////////
// Procedure : CalcJulYear
// Comment   : Berechnung des Julianischen Jahres

function CalcJulYear(Day, Month, Year: Cardinal): Double;
var
  Temp1, Temp2: Double;
begin
  Temp1 := Year / 100;
  Temp2 := 2 - Temp1 + (Temp1 / 4);
  result := (365.25 * (Year + 4716)) + (30.6001 * (Month + 1)) +
    Day + Temp2 - 1524.5;
end;

Und der Aufruf:

////////////////////////////////////////////////////////////////////////////////
// Procedure : TForm1.FormCreate
// Comment   : %

procedure TForm1.FormCreate(Sender: TObject);
begin
  edtDay.MaxLength := 2;
  edtMonth.MaxLength := 2;
  edtYear.MaxLength := 4;
  SetWindowLong(edtDay.Handle, GWL_STYLE, GetWindowLong(edtDay.Handle, GWL_STYLE)
    or ES_NUMBER);
  SetWindowLong(edtMonth.Handle, GWL_STYLE, GetWindowLong(edtMonth.Handle,
    GWL_STYLE) or ES_NUMBER);
  SetWindowLong(edtYear.Handle, GWL_STYLE, GetWindowLong(edtYear.Handle,
    GWL_STYLE) or ES_NUMBER);
  IsValidDay := False;
  IsValidMonth := False;
  IsValidYear := False;
end;

Damit habe ich vier Dinge erreicht:

  1. Der Code wird da durch generell Übersichtlicher.
  2. Kommentierung wird durch die sprechenden Variablennamen und Funktionsnamen überflüssig.
  3. Die Fehlersuche wird vereinfacht, da der eigentliche Code von der Oberfläche getrennt wurde.
  4. Die Validierung der Eingaben kann jetzt bequem in Ereignisroutinen der Controllelemente verlegt werden. Und wir haben damit gleichzeitig unser Problem in Teilprobleme zerlegt: Validierung der Eingaben, Berechnung des Datums.

Der zweite Schritt war nun die Validierung der Eingaben. Auch hier habe ich wieder die Validierung von der Oberfläche getrennt:

////////////////////////////////////////////////////////////////////////////////
// Procedure : ValidatDay
// Comment   : Tag auf Gültigkeit prüfen, die geschickte Version

function ValidateDay(const s: string): Boolean;
begin
  Result := StrToIntDef(s, 0) in [1..31];
end;

////////////////////////////////////////////////////////////////////////////////
// Procedure : ValidatMonth
// Comment   : Monat auf Gültigkeit überprüfen, die ungeschickte Version

function ValidateMonth(const s: string): Boolean;
var
  Code: Integer;
  dummy: Integer;
begin
  val(s, dummy, Code);
  if dummy <= 12 then
    result := True
  else
    result := False;
end;

Und die Eingabe:

////////////////////////////////////////////////////////////////////////////////
// Procedure : TForm1.edtDayChange
// Comment   : Bei der Eingabe Gültigkeit prüfen und prüfen ob Schaltfläche
//             zu aktiviern / deaktivieren ist

procedure TForm1.edtDayChange(Sender: TObject);
begin
  IsValidDay := ValidateDay(edtDay.text);
  EnableButton();
end;

////////////////////////////////////////////////////////////////////////////////
// Procedure : TForm1.edtMonthChange
// Comment   : Bei der Eingabe Gültigkeit prüfen und prüfen ob Schaltfläche
//             zu aktiviern / deaktivieren ist

procedure TForm1.edtMonthChange(Sender: TObject);
begin
  IsValidMonth := ValidateMonth(edtMonth.Text);
  EnableButton();
end;

////////////////////////////////////////////////////////////////////////////////
// Procedure : TForm1.edtYearChange
// Comment   : Bei der Eingabe gültigkeit prüfen und prüfen ob Schaltfläche
//             zu aktiviern / deaktivieren ist

procedure TForm1.edtYearChange(Sender: TObject);
begin
  IsValidYear := length(edtYear.Text) <> 0;
  EnableButton();
end;

Mittels dieser Validierung war es mit möglich erstens nur gültige Eingaben zu zulassen und zweites die Schaltfläche, welche die Berechnung startet, nur dann zu aktivieren, wenn alle Eingaben gültig und vom Benutzer gemacht wurden:

////////////////////////////////////////////////////////////////////////////////
// Procedure : TForm1.EnableButton
// Comment   : Schaltfläche aktivieren / deaktivieren

procedure TForm1.EnableButton;
begin
  Button1.Enabled := IsValidDay and IsValidMonth and IsValidYear;
end;

Nur wenn alle drei Statusvariablen (IsValidday, IsValidMonth, IsValidYear) wahr liefern, wird der Schaltfläche aktiviert. Auch hier habe ich auf "sprechende" Variablennamen geachtet, welche schon von sich sagen, was sie beinhalten bzw. was sie für eine Funktion haben. Zum dem sind es private Felder der Klasse, so dass ihre Sichtbarkeit auf das Nötigste beschränkt ist. Man sieht auch hier sehr schön, wie man bei diesem kleinen Programm sehr schön ohne jegliche Kommentare auskommt und der Quellcode ist dennoch verständlich und nachvollziehbar.


2010-12-29T23:44:38 +0100, mail+homepage[at]michael-puff.de