×

Clean Code – Part 1

Bereits seit einigen Jahren ist in das Bewusstsein der Informatiker und Unternehmen in der Informatikbranche gedrungen, dass die Qualität der Software neben der Ergebnisqualität auch maßgeblich durch die Codequalität bedingt ist [1,2].

Die grundlegende Frage hierbei ist jedoch, wie der Code gestaltet sein muss, damit er eine hohe Qualität aufweist. Im Wesentlichen werden die Punkte Lesbarkeit, Testbarkeit und Änderbarkeit/Wartbarkeit genannt, wenn von guter Codequalität gesprochen wird. Für diese Aspekte der Codequalität müssen Anforderungen an die Codegestaltung definiert werden, die prüfbar sind und mit denen die Erfüllung gemessen werden kann, um ein Maß der Qualität bestimmen zu können.

Im Weiteren möchte ich eine Diskussionsbasis schaffen, auf welcher aufbauend in Projektgruppen oder in einem Unternehmen eine detaillierte, individuelle Definition von Codequalität erarbeitet werden kann.

Die Fragestellung, wann Code eine hohe Qualität aufweist, welche Kennzahlen in welcher Stärke einfließen, lässt sich meiner Erfahrung nach nicht in aller Gänze allgemeingültig beantworten. Dies muss für eine Projekt- oder Unternehmensgruppe in einem gemeinsamen Prozess entstehen und weiterentwickelt werden.

Ich werde zunächst auf das grundsätzliche Verständnis von Clean Code eingehen und werde mich im Weiteren an das Buch „Clean Code“ von Robert C. Martins orientieren. [3]

1. Metaphern zu Clean Code

Robert Stephenson Smith, Gründer der Boy-Scouts 1907 hat folgenden Leitsatz im Pfadfindertum geprägt: „Wir wollen die Welt ein wenig besser verlassen, als wir sie vorgefunden haben.“ [4]

Das ist natürlich selbst für Pfadfinder ein hochgestecktes Ziel. Relativiert wird dies durch die Worte „ein wenig“. Unabhängig davon, ob nun kleine oder große Verbesserungen bewirkt werden konnten, kann der Pfadfinder das Ziel erreichen. Das führt meiner Meinung nach zu einer enormen Motivationssteigerung.

Diese Metapher wird in der Informatik für Clean Code gerne eingesetzt. Auch hier kann der Leitsatz etwas abgeändert gelten. Wir wollen den Code besser verlassen, als wir ihn vorgefunden haben. D.h. zum Beispiel, wenn der Code von einem Entwickler aufgrund einer Featureentwicklung oder einer Bugbereinigung betrachtet wird, kann und sollte der Entwickler Codezeilen, die nicht direkt für die Aufgabenbewältigung angepasst werden müssten, dennoch im Sinne von Clean Code verändern und somit verbessern.

Sehr gerne wird auch die Metapher eines Buchautors verwendet. Wenn jeder einzelne Entwickler sich als Autor eines Buches sieht und den Code, den er schreibt, als das Buch betrachtet, dann ist es ein vorranginges Ziel, dass das Geschriebene verstanden werden kann, sowie übersichtlich und vorzeigbar gestaltet ist.

Der Code ist etwas, was häufig gelesen wird. Gewisse Teile des Codes werden immer und immer wieder von denselben oder auch von anderen Entwicklern gelesen, deshalb ist es so wichtig, dass der Code einfach lesbar ist, ohne die Codezeile wiederholen zu müssen – bis verstanden wird, was damit bezweckt wird.

Im Folgenden werde ich für einige Clean Code-Kriterien Codebeispiele bringen, und greife hierzu auf die Programmiersprache Java zurück, wobei alle Beispiele ebenso auf andere Programmiersprachen übertragbar sind.

2. Aussagekräftige Namen

In Java können prinzipiell alle Artefakte benannt werden und genauso sollten auch für alle Artefakte aussagekräftige Namen gefunden werden. Darunter zählen z.B. Pakete, Klassen, Interfaces, Methoden, Attribute, etc..

Um einen Namen aussagekräftig zu machen ist es wichtig, dass

  • der Name die Absicht seiner Entität enthüllt: Der Name allein muss die Information hergeben, warum die Entität existiert, wofür sie verwendet wird und wie sie verwendet wird.
  • irreführende Informationen sollten vermieden werden:
    • Namen die mehrdeutig sind oder falsch verstanden werden können, sollten nicht verwendet werden.
      //Gegenbeispiele
      int integerValue;
      private long test123(){...}
      public void xyz(boolean bool1, boolean bool2){...}
    • Konzepte die mehrfach verwendet werden, sollten an allen Stellen des Codes auch immer gleich benannt werden.
      class UserCreator{...}
      class AccountCreator{...}
      //anstatt
      class UserGenerator{...}
      class AccountCreator{...}
  • am Namen erkannt werden kann, ob es sich um eine Methode, Klasse, Attribut, etc. handelt.
    //Attribut
    boolean isAccessActive;
    //Funktion
    private void activateAccess(){...}
    //Konstante
    public double EULERS_CONSTANT = 2.718281828459045;
    ...
  • der Name ausgesprochen werden kann.
    private int getNewValueForIteration(){...}
    //anstatt
    private int getnvfi(){...}
  • Namen verwendet werden, die leicht wiedergefunden werden können. Hierbei gilt auch, dass längere Namen besser dafür geeignet sind als kurze Namen.
  • domänenspezifische Begriffe genutzt werden, wie bspw. Patternnamen, mathematische Begriffe und so weiter.
    private void setPowertrainForMachine(Powertrain powertrain){...}

3. Funktionen

  • Die wichtigste Regel hier ist, dass der Umfang einer Funktion gemessen in der Anzahl der Zeilen möglichst klein gehalten werden soll. Als Faustregel wird hier die Länge von 20 Zeilen genannt, die möglichst nie überschritten werden soll.
  • Jeder Block in einem if-, else-, while-Statement, etc. sollte in eine Funktion ausgelagert werden.
  • Jede Funktion sollte exakt nur eine Sache erledigen – wobei hier in vielen Fällen die Schwierigkeit ist, zu definieren was genau eine Sache ist.
    private void addRowToTable(Row row) {
        ...
    }
    private void getTableRowcount() {
        long rowcount = ...;
        return rowcount;
    }
    //anstatt
    private long addRowToTableAndGetNewRowcount(Row row) {
        ...
        long rowcount = ...
        return rowcount;
    }
  • Der Name einer Funktion sollte möglichst beschreibend sein. Auch hier gilt, der Name der Funktion darf ohne Weiteres auch lang sein. Es ist besser einen langen Namen zu haben als einen kurzen kryptischen Namen. Genauso ist ein langer beschreibender Name besser als ein langer beschreibender Kommentar zur Funktion.
    public void setGlobalSamplingRatePerSecond(double globalSamplingRatePerSecond){
        ...
    }
    //anstatt
    public void setGloSampRate (double gloSampRate){
        ...
    }
  • Viel Augenmerk sollte auch auf die Argumente der Funktion gelegt werden. Was die Anzahl der Argumente betrifft, so wird dies folgendermaßen klassifiziert:
    • Ideale Anzahl an Argumenten ist Null.
    • Danach kommt Eins.
    • Nachgefolgt von Zwei.
    • Da wo es möglich ist, sollten drei Argumente bereits vermieden werden.
    • Vier Argumente sollten nur mit nachvollziehbarer Begründung verwendet werden.

Argumente die als Flag verwendet werden, um beispielsweise eine Entscheidung innerhalb einer Funktion zu treffen, sind nicht zu empfehlen. Das würde dazu führen, dass die Funktion eine Sache ausführt, wenn der Wert bspw. true ist, und eine weitere andere Sache, wenn der Wert false ist. Daraus sollten zwei Funktionen gemacht werden.

protected void setAdminUser(User user){
    ...
}
protected void setGeneralUser(User user){
    ...
}
//anstatt
protected void setUser(User user, boolean admin){
    if(admin){
        ...
    }
    else{
        ...
    }
}

Wenn es notwendig ist, können zusammengehörende Argumente in eine Klasse gewrapped werden, um diese zusammen zu fassen und die Anzahl der Argumente zu minimieren.

Circle makeCircle(Point center, double radius);
//anstatt
Circle makeCircle(double x, double y, double radius);

Auch für die Argumente gilt, aussagekräftige Namen zu verwenden. Ebenso hilft es Verben und Schlüsselwörter zu verwenden.

writeContractField(sirName);
//anstatt
write(stringValue);
  • Eine Funktion sollte keine Nebeneffekte hervorrufen. Eine Funktion, die bspw. checkPassword heißt, sollte dann keine Veränderung an Attributen oder Objekten durchführen.
    //Gegenbeispiel
    public boolean checkPassword(String password){
        //check Password
        ...
        //initialize Session
        ...
    }

Sollte es beispielsweise aus technischen Gründen nicht möglich sein, das Initialisieren der Session beim Checken der Passwortes zu unterbinden, dann sollte in diesem Ausnahmefall der Funktionsname angepasst werden und deutlich machen, dass hier mehr als nur der Check passiert.

public boolean checkPasswordAndInitializeSession(String password){
    //check Password including initialize Session 
    ...
}
  • Für den Rückgabetyp ist es wichtig, dass legitime Rückgabewerte durch das Attribut zurückgegeben werden sollten und Sonderfälle über Exceptions. Beispiel: Ein Rückgabewert von -1 sollte wenn möglich nicht verwendet werden, um anzugeben, dass die Berechnung einer mathematischen Funktion nicht definiert ist.
    public static double getRoot(double value){
        ...
        //wenn -1 throw Exception
        ...
        return root;
    }
  • Eine Funktion sollte strukturiert sein. Es sollte kein continue, break oder goto vorhanden sein. Und es ist ratsam nur ein return in einer Funktion zu haben.
    while (!fitAll){
        int area=size.width*size.height;
        while(area<totalArea){
            nextSize(size);
            area=size.width*size.height;
        }
        ....
    }
    
    //anstatt
    while (!fitAll){
        int area=size.width*size.height;
        if(area<totalArea){
            nextSize(size);
            continue;
        }
        ....
    }

4. Abschluss/Zusammenfassung

Nun bleibt noch die Frage zu klären, wie wir vorgehen können, um den Code nach diesen Kriterien zu kreieren?

Ein erster Versuch könnte sehr überdimensioniert und kompliziert sein. Das ist aber durchaus legitim, wenn der Code nicht so stehen bleibt wie er ist. Dieser Code kann als Basis verwendet werden und Schritt für Schritt überarbeitet werden, um die Codequalität zu verbessern. Wenn keine testgetriebene Entwicklung stattfindet ist es sehr ratsam, zumindest vor dem großen Überarbeiten des Codes, Tests zur Verfügung zu haben, damit sich durch das Überarbeiten keine Fehler in die Funktionalität einschleichen.

Folgende Maßnahmen können getroffen werden, um die Codequalität nachgelagert anzuheben:

  • Funktionen aufsplittern
  • Bezeichnungen neu wählen
  • Duplikate eliminieren
  • Funktionen einschrumpfen und neu sortieren
  • eventuell Klassen auseinander reißen

Es ist durchaus üblich, dass der Verbesserungsprozess über mehrere Iterationen durchgeführt wird. In diesem Artikel haben wir einleitend gesehen, dass die Codequalität ein wesentlicher Bestandteil für einen guten Code ist. Weiterhin haben wir Clean-Code-Kriterien kennengelernt, wie Bezeichner und Namen gewählt werden müssen und wie Funktionen aufgebaut sein sollten. Verdeutlicht wurden die einzelnen Kriterien mit Code-Beispielen. Für weiterführende Erklärungen empfehle ich das Buch [3] sowie diverse Webseiten und Blogs [5, 6, 7, 8, 9, 10]. Wer Interesse an einer Konferenz zu dem Thema Clean Code hat, kann die Clean Code Days 2018 im Juni in München besuchen [11].

In einem folgenden Beitrag werden wir Augenmerk auf die Kriterien von guten beziehungsweise schlechten Kommentaren im Quellcode werfen, sowie auf die Formatierung von Quellcode eingehen.

5. Quellen

[1] https://www.pluralsight.com/blog/software-development/7-reasons-clean-code-matters
[2] http://clean-code-developer.de/
[3] Clean Code: A Handbook of Agile Software Craftsmanship von Robert C. Martin. Prentice Hall International, ISBN 978-0-13-235088-4.
[4] Robert Stephenson Smith, Lord Baden-Powell, Gründer der Boy-Scouts (Pfadfinder)
[5] http://clean-code-developer.de/
[6] http://blog.cleancoder.com/
[7] https://dev.to/gonedark/a-month-of-clean-code-tips-709
[8] https://dzone.com/articles/clean-code-explanation-benefits-amp-examples
[9] https://www.pluralsight.com/blog/software-development/7-reasons-clean-code-matters
[10] https://www.codeschool.com/blog/2015/09/29/10-ways-to-write-cleaner-code/
[11] http://www.cleancode-days.de/

Von Bernhard Rimatzki | 16.04.2018
Bernhard Rimatzki

Softwareentwicklung