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)
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:
Trennt man den eigentlichen Code des Programms von der Benutzerschnittstelle, hat man drei Dinge erreicht:
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.
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.
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.
Ü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.
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:
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:
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.