· 

Wie du if-Anweisungen besser einsetzt (Clean Code)

1. Einführung

In meinem Artikel zu if-Anweisungen habe ich bereits erklärt, wofür man diese Kontrollstruktur üblicherweise einsetzt. Sie sind aus der Programmierung nicht wegzudenken und tauchen an vielen Stellen auf. Allerdings kann sich die Art, wie man sie zu welchem Zweck einsetzt, unterscheiden und ggf. den Code weniger gut lesbar machen. Deshalb möchte ich dir zeigen, wie du unter Beachtung einfacherer Regeln deine if-Anweisungen so einsetzt, dass sie deinen Code gut lesbar und vor allem wartbar machen. Die Code-Beispiele werden wir in Java programmieren.

Ein kleiner Disclaimer: diese Tipps sind subjektiver Natur. Es kann sein, dass du den Programmierstil, den ich zu optimieren versuche, präferierst. Meine Einschätzung beruht auf den Erfahrungen, die ich in den letzten Jahren beim Programmierens gemacht habe.


2. Errors First

Wir wollen in unserer Anwendung vor der Anmeldung eines Benutzers überprüfen, ob er seinen Benutzernamen und das Passwort in die dafür vorgesehenen Felder eingetragen hat, da wir ihn andernfalls nicht authentifizieren. Wir schreiben uns z. B. eine Klasse 'Anmeldeformular'.

Diese besitzt zwei Objektvariablen, nämlich benutzername und passwort (beide vom Typ String). Zudem definieren wir eine Methode daten_ausgefüllt, die einen Boolean zurückgibt und prüft, ob Nutzername und Passwort beide eingegeben wurden. 

public class Anmeldeformular {
   String benutzername;
   
   String passwort;
   
   public boolean daten_ausgefuellt() {
      if(this.benutzername != null && this.passwort != null) {
         return true;
      }
      return false;
   }
}

Wie du siehst, steht am Ende der Wert, der im Fehlerfall zurückgegeben wird. Ist es nicht viel "schöner", wenn man am Methoden-Ende keinen Fehlerfall, sondern das erwartete Ergebnis zurückgibt? Im Normalfall erwartet man bei einer Anmeldung, dass der Benutzer die dafür nötigen Daten einträgt. Man könnte also die Bedingung in der if-Anweisung umformulieren, sodass zuerst der "Fehlerfall" auftritt und am Ende das erwartete Ergebnis zurückgegeben wird. Diese Art des defensiven Programmierens kann man auch mit der einfachen Regel "Fehlerbehandlung hat Vorrang" beschreiben. Um dieses Design-Konzept für unser Beispiel zu erreichen, negierst du einfach die Bedingung unter Anwendung der De Morgan'schen Regeln und kannst dann die Rückgabewerte tauschen.

public class Anmeldeformular {
   String benutzername;
   
   String passwort;
   
   public boolean daten_ausgefuellt() {
      if(this.benutzername == null || this.passwort == null) {
         return false;
      }
      return true;
   }
}

Man kann sich bei Funktionen, die einen booleschen Ausdruck (also True oder False) zurückgeben, auch fragen, ob man für die Entscheidung, ob True oder False zurückgegeben werden soll, überhaupt eine weitere if-Anweisung braucht. Du könntest auch direkt das Ergebnis deiner if-Abfrage zurückgeben:

public class Anmeldeformular {
   String benutzername;
   
   String passwort;
   
   public boolean daten_ausgefuellt() {
      return this.benutzername != null && this.passwort != null);
   }
}

Achte aber darauf, dass der Wahrheitswert der Bedingung, deren Ergebnis du zurückgibst, mit dem Methodennamen d'accord geht. D. h. wenn die Methode daten_ausgefuellt heißt, dann sollte sie auch True zurückgeben, wenn die Daten ausgefüllt wurden. Wenn du stattdessen das Ergebnis von

this.benutzername == null || this.passwort == null

zurückgibst, wird true zurückgegeben, wenn nicht alle Daten ausgefüllt wurden, was der Logik des Namens der Methode daten_ausgefuellt widerspricht.


3. Verschachtelte Prüfungen

Verschachtelte if-Anweisungen sind der absolute Alptraum für jeden, der ein nicht funktionierendes Programm debuggen muss. Insbesondere dann, wenn du innerhalb einer if-Anweisung eine weitere if-Anweisung verwendest, die einen else-Block besitzt, der dasselbe wie die äußere if-Anweisung macht.

Mal angenommen, du möchtest eine Upload-Funktion für MP3- und PDF-Dateien schreiben. Zuerst könntest du überprüfen, ob überhaupt ein File übergeben wurde. Danach prüfst du, ob die Datei  eine MP3- oder eine PDF-Datei ist. Wenn das der Fall ist, lädst du die Datei hoch. Andernfalls brichst du z. B. ab und gibst eine Fehlermeldung aus. Was machst du in dem Fall, dass kein File übergeben wurde? Richtig, du brichst ebenfalls ab.

public void upload(File file) {
   if(file != null) {
      if(file.isMP3() || file.isPDF()) {
         upload_to_server("SERVER");
      } else {
         // Fehler und ggf. Fehlermeldjung
         return;
      }
   } else {
      // Fehler und ggf. Fehlermeldung
      return;
   }
}

Das ist noch ein sehr einfaches Programm und bereits hier hat sich eine vermeidbare Dopplung eingeschlichen. Bei komplexeren Programmen wirst du ggf. nicht mehr Herr über den Anweisungsdschungel. Dabei könnte durch  eine einfache Umstrukturierung des Codes sowohl die Dopplung, als auch die Verschachtelung vermieden werden. Wie das?

Nun, du könntest bereits in dem Moment dein Programm zum Abbruch zwingen und ggf. eine Fehlermeldung liefern, wenn kein File übergeben wurde. Dafür brauchst du nicht einmal einen else-Block:

if(file != null) {
   // Fehler und ggf. Fehlermeldung
   return;
}

In einem zweiten Schritt kannst du dann überprüfen, ob die übergebene Datei weder eine MP3-, noch eine PDF-Datei ist und in diesem Fall dein Programm abbrechen und einen Fehler zurückgeben. Du wendest hier gerade den Tipp zu Beginn dieses Artikels an, nämlich "Fehlerbehandlung hat Vorrang":

if(!file.isMP3() && !file.isPDF()) {
   // Fehler und ggf. Fehlermeldung
   return;
} 

Wenn das alles funktioniert hat, dann kannst du die Datei zum Server hochladen.

public void upload(File file) {
   if(file != null) {
      // Fehler und ggf. Fehlermeldung
      return;
   }

   if(!file.isMP3() && !file.isPDF()) {
      // Fehler und ggf. Fehlermeldung
      return;
   } 
        
   // Upload möglich.
   upload_to_server("SERVER");
}

Dieser Code ist wesentlich einfacher lesbar und kann besser gewartet werden.


4. Schalterblöcke

Wenn du viele ineinander verschachtelte if-else-Anweisungen hast, liest sich dein Code für gewöhnlich sehr schwierig. Insbesondere dann, wenn du auf einen bestimmten Wert eines Integers oder Strings prüfst, sind Schalterblöcke (Switch-Case-Statements) eine gute Alternative. Dein Programm wird so sauber strukturiert und für einen Außenstehenden besser lesbar.

Nehmen wir als Beispiel ein Programm, das für eine eingegebene Ziffer den entsprechenden Namen der Ziffer ausgibt. Wenn du das mit einer if-else-Verzweigung implementieren möchtest, dann sieht das z. B. so aus:

int ziffer = 9;
                
if(ziffer == 0) {
        System.out.println("Null");
} else if(ziffer == 1) {
        System.out.println("Eins");
} else if(ziffer == 2) {
        System.out.println("Zwei");
} else if(ziffer == 3) {
        System.out.println("Drei");
} else if(ziffer == 4) {
        System.out.println("Vier");
} else if(ziffer == 5) {
        System.out.println("Fünf");
} else if(ziffer == 6) {
        System.out.println("Sechs");
} else if(ziffer == 7) {
        System.out.println("Sieben");
} else if(ziffer == 8) {
        System.out.println("Acht");
} else {
        System.out.println("Neun");
}

Mit einem Schalterblock sparst du dir den immer wieder auftauchenden Vergleich der Variablen, auf die du prüfst, mit den entsprechenden Werten. Zudem ist das Programm durch das Hinzufügen eines weiteren Falls (Case) leichter erweiterbar.

int ziffer = 9;

switch (ziffer) {
case 0:
        System.out.println("Null");
        break;
case 1:
        System.out.println("Eins");
        break;
case 2:
        System.out.println("Zwei");
        break;
case 3:
        System.out.println("Drei");
        break;
case 4:
        System.out.println("Vier");
        break;
case 5:
        System.out.println("Fünf");
        break;
case 6:
        System.out.println("Sechs");
        break;
case 7:
        System.out.println("Sieben");
        break;
case 8:
        System.out.println("Acht");
        break;
case 9:
        System.out.println("Neun");
        break;
}