Erweiterte Programmierkonventionen

Im folgenden Artikel möchte ich einiges zum “guten Stil” beim Programmieren schreiben. Insbesondere bei größeren Projekten, wenn man mit mehreren Leuten zusammenarbeitet, ist es wichtig gut les- und wartbaren Quellcode zu produzieren. Für die Vorlesung ‘Programmierung’ ist dies hier nur bedingt relevant. Dort sind die hier definierten Konventionen ausreichend.

Es sollte klar sein, dass es keinen korrekten Programmierstil gibt. Es gibt viele Varianten, jeder Programmierer hat irgendwie seinen eigenen Stil. Ich werden versuchen euch hier meinen eigenen Programmierstil etwas näher zu bringen.

Notation

In den folgenden Quellcode-Beispielen werden Tabulatoren durch einen Punkt, gefolgt von drei Leerzeichen dargestellt.

Einrücken / Klammern

Es macht Sinn die geschweiften Klammern bei if-Anweisungen und Schleifen immer zu setzen, also auch dann wenn nur eine einzige Anweisung folgt. Sofern man korrekt einrückt ist so sofort ersichtlich, wann eine if-Anweisung bzw. Schleife endet.

Zum Einrücken sollte man Tabulatoren verwenden. Diese haben den Vorteil, dass sich jeder die Einrücktiefe selbst einstellen kann. Die meisten Programmierer verwenden eine Einrücktiefe von 4, viele aber auch 8 oder 2. Generell wird immer dann eingerückt, wenn man eine geschweifte Klammer öffnet. Zurückgerückt wird, wenn sich die Klammer wieder schließt. Dadurch lässt sich mit einem Blick erfassen, wie das Programm strukturiert ist.

Beispiel

if (42 < i && i < 48) {
.   for (int j=i; i>0; --i) {
.   .   System.out.println(j);
.   }
}
else {
.   i = 42;
}

Es gibt diverse Varianten wie und wo man die Klammern hinschreibt. Man sollte sich eine Variante aussuchen und dabei bleiben.

Hat man viele gleichartige Zeilen untereinander, kann man durch zusätzliche Leerzeichen (!) das ganze schön übersichtlich gestalten. Man rückt also ganz normal mit Tabulatoren ein und fügt dann zusätzliche Leerzeichen ein, so dass die jeweils gleichen Teile untereinander stehen. Auf diese Weise sieht das Ergebnis unabhändig von der Tabulatorbreite überall gleich aus.

Beispiel

for (int iVertZ = 0; iVertZ <= iFaces; iVertZ++) {
.   for (int iVertX = 0; iVertX <= iFaces; iVertX++) {
.   .   int x = (iNodeX * iFaces) + iVertX;
.   .   int z = (iNodeZ * iFaces) + iVertZ;
.   .   if () {
.   .   .   /* ... */
.   .   }
.   .   else {
.   .   .   final float fH = CQUADTREE_fMINHEIGHT;
.   .   .   final float fS = fHMapScale;
.   .   .   // Normalen der 6 umliegenden Dreiecke berechnen
.   .   .   CVector3 vN1 = faceNormal(x  , qRed(hmapImage.pixel(x  ,z  ))*fS+fH, z  ,
.   .   .                             x-1, qRed(hmapImage.pixel(x-1,z  ))*fS+fH, z  ,
.   .   .                             x-1, qRed(hmapImage.pixel(x-1,z-1))*fS+fH, z-1);
.   .   .   CVector3 vN2 = faceNormal(x-1, qRed(hmapImage.pixel(x-1,z  ))*fS+fH, z  ,
.   .   .                             x  , qRed(hmapImage.pixel(x  ,z  ))*fS+fH, z  ,
.   .   .                             x  , qRed(hmapImage.pixel(x  ,z+1))*fS+fH, z+1);
.   .   .   CVector3 vN3 = faceNormal(x+1, qRed(hmapImage.pixel(x+1,z+1))*fS+fH, z+1,
.   .   .                             x  , qRed(hmapImage.pixel(x  ,z+1))*fS+fH, z+1,
.   .   .                             x  , qRed(hmapImage.pixel(x  ,z  ))*fS+fH, z  );
.   .   .   CVector3 vN4 = faceNormal(x  , qRed(hmapImage.pixel(x  ,z  ))*fS+fH, z  ,
.   .   .                             x+1, qRed(hmapImage.pixel(x+1,z  ))*fS+fH, z  ,
.   .   .                             x+1, qRed(hmapImage.pixel(x+1,z+1))*fS+fH, z+1);
.   .   .   CVector3 vN5 = faceNormal(x+1, qRed(hmapImage.pixel(x+1,z  ))*fS+fH, z  ,
.   .   .                             x  , qRed(hmapImage.pixel(x  ,z  ))*fS+fH, z  ,
.   .   .                             x  , qRed(hmapImage.pixel(x  ,z-1))*fS+fH, z-1);
.   .   .   CVector3 vN6 = faceNormal(x-1, qRed(hmapImage.pixel(x-1,z-1))*fS+fH, z-1 ,
.   .   .                             x  , qRed(hmapImage.pixel(x  ,z-1))*fS+fH, z-1,
.   .   .                             x  , qRed(hmapImage.pixel(x  ,z  ))*fS+fH, z  );
.   .   }
.   }
}

Lange Zeilen / Zeilenumbrüche

Generell sollte man zu lange Zeilen vermeiden. Betrachten wir folgendes Quellcode-Beispiel mit einer extrem langen if-Bedingung.

Negativ-Beispiel

if (   ( IO.length(user_eingabe) == 2)
.   && ( 'A' <= IO.charAt(user_eingabe, 0) )
.   && ( IO.charAt(user_eingabe, 0) <= 'H' )
.   && ( '1' <= IO.charAt(user_eingabe, 1) )
.   && ( IO.charAt(user_eingabe, 1) <= '8' )
.   && ( (IO.charAt(aktuelle_pos,0))-(IO.charAt(user_eingabe,0)) ==
.        (IO.charAt(aktuelle_pos,1))-(IO.charAt(user_eingabe,1))
.     || (IO.charAt(aktuelle_pos,0))-(IO.charAt(user_eingabe,0)) ==
.       -(IO.charAt(aktuelle_pos,1))-(IO.charAt(user_eingabe,1)))
.   ) {
.   // behandle korrekte Eingabe
}
else {
.   // behandle fehlerhafte Eingabe
}

Diesen Quellcode kann man nur mit großer Mühe lesen und verstehen. In vielen Fällen kann solche langen Zeilen recht einfach durch die Verwendung von Hilfsvariabeln verkürzen. Folgendes Beispiel ist äquivalent, aber wesentlich besser lesbar:

Positiv-Beispiel

if ( IO.length(user_eingabe) != 2 ) {
.   // behandle fehlerhafte Eingabe
.   return;
}

char newX = IO.charAt(user_eingabe, 0);
char newY = IO.charAt(user_eingabe, 1);
char curX = IO.charAt(aktuelle_pos, 0);
char curY = IO.charAt(aktuelle_pos, 1);

if (    newX < 'A' || 'H' < newX
     || newY < '1' || '8' < newY
     || (    curX-newX !=  curY-newY
          && curX-newX != -curY-newY )
   ) {
.   // behandle fehlerhafte Eingabe
.   return;
}

// behandle korrekte Eingabe

In einigen Fällen ist eine solche Kürzung aber nicht möglich bzw. nicht sinnvoll möglich. In so einem Fall sollte man ab einer Länge von etwa 80-100 Zeichen die Zeile manuell umbrechen. Dabei ist darauf zu achten, dass der Rest der Zeile ebenfalls eingerückt wird. Idealerweise rückt man dabei noch etwas mehr ein, damit klar ist, dass es sich um den Rest der vorher gehenden Zeile handelt.

Beispiel

if (blabla == murks) {
.   System.out.println("Dies ist ein sinnloser Text der einfach nur sinnlos "
.   .   + "lang sein soll. Wie man sieht ist das ganze ziemlich sinnlos, aber "
.   .   + "es passt halt nicht in eine Zeile, was ja der Sinn war. Hm, hat das "
.   .   + "also doch einen Sinn?");
}

Bezeichner

Generell sollten Bezeichner (Klassen, Attributen, Methoden, Variabeln, Konstanten, …) aussagekräftig sein. Ein Bezeichner sollte kurz und knapp andeuten wozu er da ist. Weitere Beschreibung erfolgt durch Kommentare. Häufig benutzte Bezeichner sollten kürzer, weniger benutzte können auch etwas länger sein. Für kurze, prägnante Bezeichner bietet sich meist die englische Sprache an.

Allgemeine Konvention

  • Klassennamen beginnen mit einem Großbuchstaben.
  • Attribute, Methodennamen und lokale Variabeln beginnen immer mit einem Kleinbuchstaben.
  • ist ein Bezeichner aus mehreren Wörtern zusammengesetzt, schreibt man die Anfangsbuchstaben der einzelnen Wörtern groß (Camel-Notation)
  • Konstanten, Makros, etc. werden komplett in Großbuchstaben geschrieben. Dabei werden einzelne Wörter im Namen durch einen Unterstrich getrennt.

Beispiele

// Schleifenzähler
i, j, k, x, y

// Attribute, Variabeln
pstmt           // Objekt der Klasse 'PreparedStatement'
rs              // Objekt der Klasse 'ResultSet'
userJID         // JabberID eines Benutzers
usernames       // Ein Array von Benutzernamen
haveAdminRights // boolean der angibt ob der Benutzer Adminrechte hat

// Methoden
getAverage(), computeResult(), addElement(), openConnection(), closeConnection()

// Konstanten
MAX_LIST_SIZE, DEFAULT_PLAYERNAME

// Makros
DEBUG_MODE, OS_LINUX, OS_WINDOWS

Ungarische Notation

Eine verbreitete Konvention ist es, Abkürzungen für den Typ eines Bezeichners als Präfix mit in den Bezeichner aufzunehmen. Es gibt viele Varianten. Wichtig ist, dass man sich eine Variante aussucht und diese entweder konsequent mit allen Mitarbeitern im kompletten Projekt durchzieht oder es komplett bleiben lässt. Bei Dingen wie Schleifenzählern (i, j, …) wendet man die Notation normalerweise nicht an.

i (int)
f (float)
d (double)
c (char)
b (boolean)
s (String)
p (Pointer)
itr (Iterator)
m_ (für Attribute, auch Member genannt)
C (Klasse) (nicht für Objektvariablen)
E (Enum)
I (Interface)

Beispiele

iCounter, dSum, sUsername, CResourceManager, EState

Kommentare / Sprache

Gerade in größeren Projekten sollte man nicht zu viel, aber auch nicht zu wenig kommentieren. Sobald man zum Verstehen des Quellcodes weniger Zeit braucht als zum Lesen des Kommentars, macht der Kommentar offensichtlich keinen Sinn mehr.

Bei Methoden/Klassen sollte man aber generell Voraussetzungen, Spezialfälle, Rückgabewerte im Fehlerfall, usw. dokumentieren. Im Quellcode selbst sollte man längere zusammengehörige Passagen und natürlich komplizierte Stellen kommentieren. Extrem wichtig ist es, dass Kommentare korrekt sind. Ändert man den Quellcode, sollte man sofort auch den Kommentar anpassen.

Für größere Projekte bietet sich die Verwendung von Dokumentations-Programmen wie Doxygen oder JavaDoc an. Auch sollte man Kommentare grundsätzlich in englischer Sprache verfassen. Zum einen hat man so keine Probleme mit Umlauten, zum anderen kann jeder andere Programmierer den Quellcode verstehen. Was würde dir beispielsweise der Quellcode eines noch so guten Programms nützen, wenn er komplett in französisch, polnisch, ungarisch, chinesisch etc. kommentiert ist? Nicht jeder Mensch auf dieser Welt kann deutsch, aber sehr viele können englisch. Man weiß nie mit wem man irgendwann mal über das Internet zusammenarbeitet und wann man Quellcode-Ausschnitte für ein anderes Projekt noch einmal verwendet.

Vermeiden von Verschachtelung

Sofern es möglich ist sollte man unnötige Verschachtelung vermeiden. Häufig muss man z.B. Benutzereingaben in mehreren Schritten prüfen. Fängt man z.B. die einzelnen Fehler mit einer if-Anweisung ab, so erhält man unnötig tief verschachtelten Code.

Negativ-Beispiel

Iterator<String> itrUsers = usernames.iterator();
while (itrUsers.hasNext()) {
.   String username = itrUsers.next();
.   if (username.indexOf('@') == -1) {
.   .   try {
.   .   .   JID jUserJID;
.   .   .   jUserJID = new JID(username, helgaUtil.getServerName(), null);

.   .   .   if (userManager.isRegisteredUser(username)) {
.   .   .   .   if (haveAdminRights(context, true) {

.   .   .   .   .   /* working with valid admin username
.   .   .   .   .    *
.   .   .   .   .    * ... 100 lines of code ...
.   .   .   .   .    *
.   .   .   .   .    */
.   .   .   .   }
.   .   .   .   else {
.   .   .   .   .   System.out.println("No Admin rights!");
.   .   .   .   .   continue;
.   .   .   .   }
.   .   .   }
.   .   .   else {
.   .   .   .   System.out.println("Not an registered user!");
.   .   .   .   continue;
.   .   .   }
.   .   }
.   .   catch (IllegalArgumentException e) {
.   .   .   System.out.println("Illegal username!");
.   .   .   continue;
.   .   }
.   }
.   else {
.   .   System.out.println("Illegal username!");
.   .   continue;
.   }
}

Behandelt man Fehler sofort und nicht erst im else-Teil, erhält man in der Regel übersichtlicheren Quellcode. Meistens springt man mit einem return, break oder continue aus dem Code heraus und spart sich so einen else-Teil und somit das einrücken das folgenden Quellcodes.

Positiv-Beispiel

Iterator<String> itrUsers = usernames.iterator();
while (itrUsers.hasNext()) {
.   String username = itrUsers.next();
.   if (username.indexOf('@') != -1) {
.   .   System.out.println("Illegal username!");
.   .   continue;
.   }

.   JID jUserJID = null;
.   try {
.   .   jUserJID = new JID(username, helgaUtil.getServerName(), null);
.   }
.   catch (IllegalArgumentException e) {
.   .   System.out.println("Illegal username!");
.   .   continue;
.   }

.   if (!userManager.isRegisteredUser(username)) {
.   .   System.out.println("Not an registered user!");
.   .   continue;
.   }

.   if (!haveAdminRights(context, true) {
.   .   System.out.println("No Admin rights!");
.   .   continue;
.   }

.   /* working with valid admin username
.    *
.    * ... 100 lines of code ...
.    *
.    */
}

Vermutlich kommen jetzt wieder die Goto-Verweigerer und sagen, ein Programm, welches Sprünge enthält, sei unübersichtlicher Spaghetti-Code. Nun ja, die Frage ist doch, was ist übersichtlicher? Eine tiefe Verschachtelung mit 10 oder 15facher Einrückung oder ein paar Sprünge mit ‘return’, ‘break’ oder ‘continue’. Meiner Meinung nach ist es bei diesen drei Sprunganweisungen immer sofort klar wo der Kontrollfluss weiter läuft. Klar ist natürlich, dass man es mit den Sprüngen nicht übertreiben sollte, insbesondere wenn man Labels benutzt.