Code prüfen mit dem Fünfsekundenexperiment

Das Fünfsekundenexperiment: Guter Code, schlechter Code?

Markus Kiss, Christian Kumpe

@shutterstock/Leszek Glasner

Was ist eigentlich guter Code? Wahrscheinlich haben sich viele Entwickler diese Frage schon gestellt und werden sie auch in Zukunft stellen. Bücher wie „Clean Code“ von Robert C. Martin [1] enthalten viele Regeln, die Code befolgen sollte. Aber was ist eigentlich die Essenz des Ganzen? Wir sagen einfach: „Guter Code muss lesbar sein“.

Bei Vorstellungsgesprächen stellen wir Bewerbern eine kleine zweistündige Programmieraufgabe. Für ein Interface müssen sie einige Methoden implementieren, wahlweise in Java oder C#. Weitreichende technische Kenntnisse sind dafür nicht erforderlich. Das Wissen über das Java-Standard-API, hauptsächlich das Collections Framework, oder deren C#-Äquivalente reichen vollkommen aus. Außerdem dürfen sie während der Aufgabe ins Internet. Im Anschluss soll der Bewerber seinen Code kurz erklären und seine Designentscheidungen begründen.

Als Betreuer für diese Aufgabe ist uns bald aufgefallen, dass mancher Code schon beim ersten Durchsehen zu verstehen ist, sogar während man den Erklärungen des Bewerbers folgt. Kleine Fehler fallen sofort auf und man kann schnell in die Diskussion einsteigen. Manchmal ist der Code aber auch bei genauem Hinsehen einfach nicht verständlich und man braucht mehrere Minuten, bis man den Ablauf einzelner Methoden versteht, um dem Bewerber Feedback über mögliche Fehler zu geben.

Code prüfen mit dem Fünfsekundenexperiment

Daraus entstand bei uns die Idee des Fünfsekundenexperiments. Listing 1 zeigt ein Stück Code, das wir so ähnlich einmal während einer Programmieraufgabe präsentiert bekamen. Betrachten wir das Beispiel, legen das Heft nach etwa fünf Sekunden kurz beiseite und überlegen, was der Code macht, machen sollte und wo sich mögliche Fehler verstecken.

public void printSortedByAge() {
  Iterator iter = customers.iterator();
  ArrayList list = new ArrayList();
  while (iter.hasNext()) {
    Customer customer = iter.next();
    list.add(customer.getAge() + " " + customer.getLoginName());
  }
  Collections.sort(list);
  Iterator iter2 = list.iterator();
  while (iter2.hasNext()) {
    try {
      String str = iter2.next();
      System.out.println(findCustomerByName(str.substring(str.lastIndexOf(" ") + 1)));
    } catch (CustomerNotFoundException e) {
      // TODO Auto-generated catch block
      e.printStackTrace();
    }
  }
}

Was macht der Code?

Am aussagekräftigsten ist wahrscheinlich noch der Methodenname. Kennt man auch das zu implementierende Interface, ist klar: Hier sollen Kunden nach Alter sortiert ausgegeben werden.

Aber wie macht das der Code und funktioniert die Implementierung? Wo liegen ihre Grenzen? Das kann wohl kaum jemand innerhalb weniger Sekunden beurteilen. Ohne jetzt zu sehr ins Detail zu gehen: Die Kunden werden sortiert, indem Strings der Form Alter Log-in-Name gebildet werden. Diese Strings werden anschließend in sortierter Reihenfolge abgearbeitet, wobei der Log-in-Name aus dem String extrahiert und anschließend der entsprechende Kunde anhand seines Log-in-Namens geholt und ausgegeben wird. Weitere Details zu diesem Codebeispiel finden sich auf Github.

Dieser Lösungsweg ist sicher nicht einfach zu erfassen, und auch mögliche Fehler sind nicht direkt offensichtlich. Aber solange die Kunden zwischen 10 und 99 Jahre alt sind und im Log-in-Namen keine Leerzeichen erlaubt sind, funktioniert es.

In der Kürze liegt die Würze

Nun wollen wir das Listing 2 etwa fünf Sekunden betrachten. Dem Methodennamen nach sollte er genau das Gleiche tun, aber auch ohne diese Hilfe sind die drei zentralen Schritte sehr schnell zu erfassen: Die Liste der Kunden wird kopiert, die Kopie mithilfe des AgeComparators nach dem Alter sortiert und anschließend in einer Schleife auf der Konsole ausgegeben.

public void printSortedByAge() {
  List customersToSort = new ArrayList<>(customers);
  Collections.sort(customersToSort, new AgeComparator());
  for (Customer customer : customersToSort) {
    System.out.println(customer);
  }
}

Der Code ist aus vielen Gründen deutlich einfacher zu lesen und schneller zu erfassen: Er ist kürzer, enthält keinen unnötigen Code und geht so ziemlich den direktesten Weg. Durch sprechende Variablennamen und gute Strukturierung ist die Methode auch ohne jeden Kommentar schnell zu verstehen.

Nicht zu viel kommentieren

Auch gut gemeintes Kommentieren von Code kann schnell neue Probleme aufwerfen, was Listing 3 verdeutlicht.

/**
 * Deletes the given {@link Customer}.
 * @param argCustomer customer to delete
 * @return true if the customer has been deleted, false otherwise.
 */
@Override
public boolean deleteCustomer(Customer argCustomer) {
  // Check if we have a customer in our list.
  // If we don't have one, we return false as 
  // the customer hasn't been deleted actually.
  if (containsCustomer(argCustomer)) {
    // Remove the customer.
    // We could also directly return the result of remove() 
    customers.remove(argCustomer);
    // Return true to communicate the result
    return true;
  }
  // Return false if the customer could not be found
  return false;
}

Die Methode deleteCustomer enthält zwar nur wenig Logik, doch über jeder Codezeile steht ein Kommentar, der nochmals in Prosa beschreibt, was der darauffolgende Code macht. Dies bläht den Code nicht nur optisch auf, sondern zwingt andere Entwickler dazu, neben dem Code auch die Kommentare zu lesen und zu verstehen – ein zeitaufwändiger Prozess. Passt dann noch aufgrund von Refactorings der Code nicht mehr zu den Kommentaren, ist Verwirrung vorprogrammiert. Wir fragen uns dann zum Beispiel, ob der Code etwa fehlerhaft ist, weil der Kommentar etwas anderes ausdrückt. Oder ist der Kommentar selbst falsch?

Diese Irritationen können wir von vorneherein vermeiden, indem wir den Code bereits so klar strukturieren, dass keine Kommentare zur Erläuterung nötig sind. Dies lässt sich zum Beispiel durch aussagekräftige Klassen-, Variablen- und Methodennamen erreichen. Guter Code erzählt von alleine, was er macht.

Im Beispiel ist auch ein Javadoc-Block aufgeführt, der den Methodenzweck inklusive der Ein- und Ausgabewerte beschreibt. Diese Art von Kommentaren ist wiederum ganz hilfreich, um die öffentliche Schnittstelle zu dokumentieren und insbesondere auf semantische Sachverhalte hinzuweisen, z. B. wann true und wann false zurückgeliefert wird.

Code ordentlich strukturieren

Neben aussagekräftigen Bezeichnern spielt auch die Strukturierung und Formatierung des Codes eine wesentliche Rolle. In Listing 4 ist die Implementierung der Methode findMaleAdultCustomers zu sehen, die aus einer Liste von Kunden diejenigen heraussucht und zurückgibt, die männlich und erwachsen sind.

public List findMaleAdultCustomers() {
  List custFound = new ArrayList<>();
  for (int i = 0; i < custList.size(); i++) {
  if (custList.get(i).getAge() < 18) 
      continue;
    else
    {
      if (custList.get(i).getGender() != Gender.FEMALE) 
      custFound.add(custList.get(i)); }
    }
  return custFound;
}

Beim ersten Blick fällt auf, dass die Struktur des Codes und der Programmfluss nicht sofort ersichtlich ist: Es ist unklar, welchen Block die for-Schleife umfasst und wo die if– und else-Blöcke hingehören. Sinnvolle Einrückungen und Formatierungen nach allgemein bekannten Konventionen schaffen hier Abhilfe. In Entwicklungsumgebungen wie Eclipse, IntelliJ IDEA und NetBeans ist die Autoformatierung für Java-Code bereits mitgeliefert.

Darüber hinaus fällt auf, dass die Ausdrücke in den if-Abfragen jeweils den Negativfall prüfen: Es wird geprüft, ob das Alter des Kunden kleiner als 18 ist und der Kunde keine Frau ist. Insbesondere die Prüfung des Alters macht den weiteren Ablauf komplex, da der aktuelle Schleifendurchlauf per continue übersprungen wird. Die positive Formulierung des Ausdrucks „Alter des Kunden ist größer-gleich 18“ würde viel klarer ausdrücken, dass im weiteren Ablauf mit volljährigen Kunden gearbeitet wird. Ebenso würde die Prüfung „Kunde ist männlich“ direkt beschreiben, dass man nur diese Kunden im Fokus hat.

In Listung 5 ist dieselbe Funktionalität implementiert, jedoch auf deutlich übersichtlichere Art und Weise: Zum einen wird eine for-each-Schleife verwendet, um deutlicher zu machen, dass über eine Collection iteriert wird. Zum anderen wurden die if-Ausdrücke in eigene boolesche Methoden extrahiert, die durch ihren Namen ausdrücken, was sie prüfen. Dadurch liest sich der if-Ausdruck wie Prosa: „Wenn der Kunde männlich und erwachsen ist, dann…“. Bereits diese kleinen Refactorings führen dazu, dass der Kontrollfluss der Methode auf einen Blick erfasst werden kann.

public List findMaleAdultCustomers() {
  List maleAdultCustomers = new ArrayList<>();
  for (Customer customer : customers) {
    if (isMale(customer) && isAdult(customer)) {
      maleAdultCustomers.add(customer);
    }
  }
  return maleAdultCustomers;
}
private boolean isMale(Customer customer) {
  return customer.getGender() == Gender.MALE;
}
private boolean isAdult(Customer customer) {
  return customer.getAge() >= 18;
}

Unnötigen Code vermeiden

Ein weiteres Problem ist die Verwendung von überflüssigem Code. Was meinen wir damit? Listing 6 zeigt gleich mehrere dieser Fälle auf, z. B. der parameterlose Konstruktor: Dieser ruft nur super() auf, ohne weitere Initialisierungen vorzunehmen. In diesem Fall ist die Deklaration des Konstruktors vollkommen überflüssig, da ein solcher laut Java Language Specification sowieso automatisch generiert wird. Ebenfalls unnötig ist das explizite Prüfen auf die booleschen Werte true oder false in if-Anweisungen. Mit ein bisschen Refactoring werden wir feststellen, dass sich die komplette Methode containsCustomer auf eine einzige Anweisung reduzieren lässt: return customers.contains(argCustomer). Am schönsten ist es immer, wenn man Code löschen kann, ohne etwas am Verhalten zu ändern. Der Vorteil liegt auf der Hand: Wir müssen keinen Code mehr lesen, der sowieso automatisch im Hintergrund durch den Compiler erzeugt wird und können uns auf die wesentlichen Abläufe konzentrieren.

public CustomerServiceImpl() {  
  super();
}
public boolean containsCustomer(final Customer argCustomer) {
  if (customers.contains(argCustomer) == true) {
    return true;  
  } else {
    return false;  
  }
}

Keine Überraschungen

Das „Principle of Least Astonishment“ ist ein Prinzip aus dem Clean Code [1], das im Kern aussagt, dass geschriebener Code keine Überraschungen jeglicher Art mit sich bringen sollte. Eine solche Überraschung versteckt sich in Listing 7: Hier wird die Collection customers nach dem Nachnamen des Kunden sortiert auf der Konsole ausgegeben.

public void printSortedByLastName() {  
  SortedSet sortedCustomers = new TreeSet(new LastNameComparator());  
  sortedCustomers.addAll(customers);
  for (Customer customer : sortedCustomers) {
    System.out.println(customer);  
  }
}

Eine Frage: Wo findet das Sortieren eigentlich statt? Java-Kenner werden wissen, dass dies in der TreeSet-Implementierung intern beim Aufruf von addAll() erfolgt – unter Verwendung des LastNameComparator. Wichtig ist, welche Auswirkungen der Einsatz eines TreeSet hat: Da ein Set per Definition keine Duplikate erlaubt, würde nur ein Teil der Kunden auf der Konsole ausgegeben werden. Hätte man z. B. drei Kunden mit Nachnamen „Meier“, würde nur einer davon ausgegeben werden – selbst wenn alle drei Kunden unterschiedliche Vornamen hätten. Zweifelsohne eine Überraschung, die vielleicht erst im Livebetrieb aufgefallen wäre, wenn Kundendatensätze fehlen. Sinnvoller wäre es, das Sortieren explizit mittels Collections.sort() durchführen zu lassen (analog zu Listing 1), um solche unangenehmen Überraschungen zu vermeiden und gleichzeitig die Lesbarkeit des Codes zu verbessern.

Fazit: Guter Code ist gut lesbarer Code

Wenn wir uns nun abschließend die Frage stellen: „Was ist guter Code, was ist schlechter Code?“, können wir zwar viele Regeln und Prinzipien aufzählen, doch alle führen zu derselben einfachen wie wirksamen Kernaussage: „Guter Code ist lesbar, schlechter Code ist es nicht.“ Als Entwickler verbringen wir im Schnitt 80 Prozent unserer Zeit damit, bestehenden Code zu lesen und nur 20 Prozent mit dem tatsächlichen Schreiben von neuem Code. Schon deshalb lohnt es sich, beim Schreiben von Code darauf zu achten, dass andere Entwickler später möglichst wenig Zeit benötigen, ihn zu verstehen.

Aufmacherbild: Thumb up and down von Shutterstock / Urheberrecht: Leszek Glasner

Verwandte Themen:

Geschrieben von
Markus Kiss
Markus Kiss
Markus Kiss hat in Karlsruhe und Mannheim Informatik studiert und arbeitet als Senior Softwareentwickler bei der Netpioneer GmbH in Karlsruhe. Er interessiert sich seit mehreren Jahren für Clean Code und saubere Softwarearchitekturen.
Christian Kumpe
Christian Kumpe
Christian Kumpe studierte Informatik am Karlsruher Institut für Technologie (KIT) und sammelte bereits während seines Studiums als Freelancer Erfahrung in diversen Java-Projekten. Seit 2011 arbeitet er als Softwareentwickler bei der Netpioneer GmbH in Karlsruhe. Seine aktuellen Themenschwerpunkte sind Java-basierte Portale und Internetplattformen.
Kommentare

Hinterlasse einen Kommentar

2 Kommentare auf "Das Fünfsekundenexperiment: Guter Code, schlechter Code?"

avatar
400
  Subscribe  
Benachrichtige mich zu:
Martin Dieblich
Gast

Eine sehr interessantes Konzept! Ich denke das Fünfsekundenexperiment lässt sich auch gut auf andere Bereiche erweitern, wie Architekturen, (UML-)Modelle, Paketstrukturen usw….

Alex Schneider
Gast

Ich denke auch, dass es sich auf andere Branchen erweitern lässt.