Subsections of Programmiermethoden und Clean Code

Javadoc

TL;DR

Mit Javadoc kann aus speziell markierten Block-Kommentaren eine externe Dokumentation im HTML-Format erzeugt werden. Die Block-Kommentare, auf die das im JDK enthaltene Programm javadoc reagiert, beginnen mit /** (also einem zusätzlichen Stern, der für den Java-Compiler nur das erste Kommentarzeichen ist).

Die erste Zeile eines Javadoc-Kommentars ist eine "Zusammenfassung" und an fast allen Stellen der generierten Doku sichtbar. Diese Summary sollte kurz gehalten werden und eine Idee vermitteln, was die Klasse oder die Methode oder das Attribut macht.

Für die Dokumentation von Parametern, Rückgabetypen, Exceptions und veralteten Elementen existieren spezielle Annotationen: @param, @return, @throws und @deprecated.

Als Faustregel gilt: Es werden alle public und protected Elemente (Klassen, Methoden, Attribute) mit Javadoc kommentiert. Alle nicht-öffentlichen Elemente bekommen normale Java-Kommentare (Zeilen- oder Blockkommentare).

Videos (YouTube)
Videos (HSBI-Medienportal)
Lernziele
  • (K2) Ziel der Javadoc-Dokumentation verstehen
  • (K2) Typischen Aufgabe von Javadoc-Kommentaren verstehen
  • (K3) Dokumentation öffentlich sichtbarer Elemente mit Javadoc
  • (K3) Schreiben einer sinnvollen Summary
  • (K3) Einsatz von Annotationen zur Dokumentation von Parametern, Rückgabetypen, Exceptions, veralteten Elementen

Dokumentation mit Javadoc

/**
 * Beschreibung Beschreibung (Summary).
 *
 * <p>Hier kommt dann ein laengerer Text, der die Dinge
 * bei Bedarf etwas ausfuehrlicher erklaert.
 */
public void wuppie() {}

Javadoc-Kommentare sind (aus Java-Sicht) normale Block-Kommentare, wobei der Beginn mit /** eingeleitet wird. Dieser Beginn ist für das Tool javadoc (Bestandteil des JDK, genau wie java und javac) das Signal, dass hier ein Kommentar anfängt, den das Tool in eine HTML-Dokumentation übersetzen soll.

Typischerweise wird am Anfang jeder Kommentarzeile ein * eingefügt; dieser wird von Javadoc ignoriert.

Sie können neben normalem Text und speziellen Annotationen auch HTML-Elemente wie <p> und <code> oder <ul> nutzen.

Mit javadoc *.java können Sie in der Konsole aus den Java-Dateien die Dokumentation generieren lassen. Oder Sie geben das in Ihrer IDE in Auftrag ... (die dann diesen Aufruf gern für Sie tätigt).

Standard-Aufbau

/**
 * Beschreibung Beschreibung (Summary).
 *
 * <p> Hier kommt dann ein laengerer Text, der die Dinge
 * bei Bedarf etwas ausfuehrlicher erklaert.
 *
 * @param   date  Tag, Wert zw. 1 .. 31
 * @return  Anzahl der Sekunden seit 1.1.1970
 * @throws  NumberFormatException
 * @deprecated As of JDK version 1.1
 */
public int setDate(int date) {
    setField(Calendar.DATE, date);
}
  • Erste Zeile bei Methoden/Attributen geht in die generierte "Summary" in der Übersicht, der Rest in die "Details"
    • Die "Summary" sollte kein kompletter Satz sein, wird aber wie ein Satz geschrieben (Groß beginnen, mit Punkt beenden). Es sollte nicht beginnen mit "Diese Methode macht ..." oder "Diese Klasse ist ...". Ein gutes Beispiel wäre "Berechnet die Steuerrückerstattung."
    • Danach kommen die Details, die in der generierten Dokumentation erst durch Aufklappen der Elemente sichtbar sind. Erklären Sie, wieso der Code was machen soll und welche Designentscheidungen getroffen wurden (und warum).
  • Leerzeilen gliedern den Text in Absätze. Neue Absätze werden mit einem <p> eingeleitet. (Ausnahmen: Wenn der Text mit <ul> o.ä. beginnt oder der Absatz mit den Block-Tags.)
  • Die "Block-Tags" @param, @return, @throws, @deprecated werden durch einen Absatz von der restlichen Beschreibung getrennt und tauchen in exakt dieser Reihenfolge auf. Die Beschreibung dieser Tags ist nicht leer - anderenfalls lässt man das Tag weg. Falls die Zeile für die Beschreibung nicht reicht, wird umgebrochen und die Folgezeile mit vier Leerzeichen (beginnend mit dem @) eingerückt.
    • Mit @param erklären Sie die Bedeutung eines Parameters (von links nach rechts) einer Methode. Beispiel: @param date Tag, Wert zw. 1 .. 31. Wiederholen Sie dies für jeden Parameter.
    • Mit @return beschreiben Sie den Rückgabetyp/-wert. Beispiel: @return Anzahl der Sekunden seit 1.1.1970. Bei Rückgabe von void wird diese Beschreibung weggelassen (die Beschreibung wäre dann ja leer).
    • Mit @throws geben Sie an, welche "checked" Exceptions die Methode wirft.
    • Mit @deprecated können Sie im Kommentar sagen, dass ein Element veraltet ist und möglicherweise mit der nächsten Version o.ä. entfernt wird. (siehe nächste Folie)

=> Dies sind die Basis-Regeln aus dem populären Google-Java-Style [googlestyleguide].

Veraltete Elemente

/**
 * Beschreibung Beschreibung Beschreibung.
 *
 * @deprecated As of v102, replaced by <code>Foo.fluppie()</code>.
 */
@Deprecated
public void wuppie() {}
  • Annotation zum Markieren als "veraltet" (in der generierten Dokumentation): @deprecated
  • Für Sichtbarkeit zur Laufzeit bzw. im Tooling/IDE: normale Code-Annotation @Deprecated

Dies ist ein guter Weg, um Elemente einer öffentlichen API als "veraltet" zu kennzeichnen. Üblicherweise wird diese Kennzeichnung für einige wenige Releases beibehalten und danach das veraltete Element aus der API entfernt.

Autoren, Versionen, ...

/**
 * Beschreibung Beschreibung Beschreibung.
 *
 * @author  Dagobert Duck
 * @version V1
 * @since   schon immer
 */
  • Annotationen für Autoren und Version: @author, @version, @since

Diese Annotationen finden Sie vor allem in Kommentaren zu Packages oder Klassen.

Was muss kommentiert werden?

  • Alle public Klassen

  • Alle public und protected Elemente der Klassen

  • Ausnahme: @Override (An diesen Methoden kann, aber muss nicht kommentiert werden.)

Alle anderen Elemente bei Bedarf mit normalen Kommentaren versehen.

Beispiel aus dem JDK: ArrayList

Schauen Sie sich gern mal Klassen aus der Java-API an, beispielsweise eine java.util.ArrayList:

Best Practices: Was beschreibe ich eigentlich?

Unter Documentation Best Practices finden Sie eine sehr gute Beschreibung, was das Ziel der Dokumentation sein sollte. Versuchen Sie, dieses zu erreichen!

Wrap-Up

  • Javadoc-Kommentare sind normale Block-Kommentare beginnend mit /**

  • Generierung der HTML-Dokumentation mit javadoc *.java

  • Erste Zeile ist eine Zusammenfassung (fast immer sichtbar)

  • Längerer Text danach als "Description" einer Methode/Klasse

  • Annotationen für besondere Elemente: @param, @return, @throws, @deprecated

  • Faustregel: Alle public und protected Elemente mit Javadoc kommentieren!

Challenges

Betrachten Sie die Javadoc einiger Klassen im Dungeon-Projekt: dojo.rooms.LevelRoom, dojo.rooms.MonsterRoom, und contrib.components.HealthComponent.

Stellen Sie sich vor, Sie müssten diese Klassen in einer Übungsaufgabe nutzen (das könnte tatsächlich passieren!) ...

Können Sie anhand der Javadoc verstehen, wozu die drei Klassen dienen und wie Sie diese Klassen benutzen sollten? Vergleichen Sie die Qualität der Dokumentation. Was würden Sie gern in der Dokumentation finden? Was würden Sie ändern?

Quellen

Logging

TL;DR

Im Paket java.util.logging findet sich eine einfache Logging-API.

Über die Methode getLogger() der Klasse Logger (Factory-Method-Pattern) kann ein (neuer) Logger erzeugt werden, dabei wird über den String-Parameter eine Logger-Hierarchie aufgebaut analog zu den Java-Package-Strukturen. Der oberste Logger (der "Root-Logger") hat den leeren Namen.

Jeder Logger kann mit einem Log-Level (Klasse Level) eingestellt werden; Log-Meldungen unterhalb des eingestellten Levels werden verworfen.

Vom Logger nicht verworfene Log-Meldungen werden an den bzw. die Handler des Loggers und (per Default) an den Eltern-Logger weiter gereicht. Die Handler haben ebenfalls ein einstellbares Log-Level und verwerfen alle Nachrichten unterhalb der eingestellten Schwelle. Zur tatsächlichen Ausgabe gibt man einem Handler noch einen Formatter mit. Defaultmäßig hat nur der Root-Logger einen Handler.

Der Root-Logger (leerer String als Name) hat als Default-Level (wie auch sein Console-Handler) "Info" eingestellt.

Nachrichten, die durch Weiterleitung nach oben empfangen wurden, werden nicht am Log-Level des empfangenden Loggers gemessen, sondern akzeptiert und an die Handler des Loggers und (sofern nicht deaktiviert) an den Elternlogger weitergereicht.

Videos (HSBI-Medienportal)
Lernziele
  • (K3) Nutzung der Java Logging API im Paket java.util.logging
  • (K3) Erstellung eigener Handler und Formatter

Wie prüfen Sie die Werte von Variablen/Objekten?

  1. Debugging

    • Beeinflusst Code nicht
    • Kann schnell komplex und umständlich werden
    • Sitzung transient - nicht wiederholbar
  2. "Poor-man's-debugging" (Ausgaben mit System.out.println)

    • Müssen irgendwann entfernt werden
    • Ausgabe nur auf einem Kanal (Konsole)
    • Keine Filterung nach Problemgrad - keine Unterscheidung zwischen Warnungen, einfachen Informationen, ...
  3. Logging

    • Verschiedene (Java-) Frameworks: java.util.logging (JDK), log4j (Apache), SLF4J, Logback, ...

Java Logging API - Überblick

Paket java.util.logging

Eine Applikation kann verschiedene Logger instanziieren. Die Logger bauen per Namenskonvention hierarchisch aufeinander auf. Jeder Logger kann selbst mehrere Handler haben, die eine Log-Nachricht letztlich auf eine bestimmte Art und Weise an die Außenwelt weitergeben.

Log-Meldungen werden einem Level zugeordnet. Jeder Logger und Handler hat ein Mindest-Level eingestellt, d.h. Nachrichten mit einem kleineren Level werden verworfen.

Zusätzlich gibt es noch Filter, mit denen man Nachrichten (zusätzlich zum Log-Level) nach weiteren Kriterien filtern kann.

Erzeugen neuer Logger

import java.util.logging.Logger;
Logger l = Logger.getLogger(MyClass.class.getName());
  • Factory-Methode der Klasse java.util.logging.Logger

    public static Logger getLogger(String name);

    => Methode liefert bereits vorhandenen Logger mit diesem Namen (sonst neuen Logger)

  • Best Practice: Nutzung des voll-qualifizierten Klassennamen: MyClass.class.getName()

    • Leicht zu implementieren
    • Leicht zu erklären
    • Spiegelt modulares Design
    • Ausgaben enthalten automatisch Hinweis auf Herkunft (Lokalität) der Meldung
    • Alternativen: Funktionale Namen wie "XML", "DB", "Security"

Ausgabe von Logmeldungen

public void log(Level level, String msg);
  • Diverse Convenience-Methoden (Auswahl):

    public void warning(String msg)
    public void info(String msg)
    public void entering(String srcClass, String srcMethod)
    public void exiting(String srcClass, String srcMethod)
  • Beispiel

    import java.util.logging.Logger;
    Logger l = Logger.getLogger(MyClass.class.getName());
    l.info("Hello World :-)");

Wichtigkeit von Logmeldungen: Stufen

  • java.util.logger.Level definiert 7 Stufen:

    • SEVERE, WARNING, INFO, CONFIG, FINE, FINER, FINEST (von höchster zu niedrigster Prio)
    • Zusätzlich ALL und OFF
  • Nutzung der Log-Level:

    • Logger hat Log-Level: Meldungen mit kleinerem Level werden verworfen
    • Prüfung mit public boolean isLoggable(Level)
    • Setzen mit public void setLevel(Level)

=> Warum wird im Beispiel nach log.setLevel(Level.ALL); trotzdem nur ab INFO geloggt? Wer erzeugt eigentlich die Ausgaben?!

Jemand muss die Arbeit machen ...

  • Pro Logger mehrere Handler möglich

    • Logger übergibt nicht verworfene Nachrichten an Handler
    • Handler haben selbst ein Log-Level (analog zum Logger)
    • Handler verarbeiten die Nachrichten, wenn Level ausreichend
  • Standard-Handler: StreamHandler, ConsoleHandler, FileHandler

  • Handler nutzen zur Formatierung der Ausgabe einen Formatter

  • Standard-Formatter: SimpleFormatter und XMLFormatter

=> Warum wird im Beispiel nach dem Auskommentieren von log.setUseParentHandlers(false); immer noch eine zusätzliche Ausgabe angezeigt (ab INFO aufwärts)?!

Ich ... bin ... Dein ... Vater ...

  • Logger bilden Hierarchie über Namen

    • Trenner für Namenshierarchie: "." (analog zu Packages) => mit jedem "." wird eine weitere Ebene der Hierarchie aufgemacht ...
    • Jeder Logger kennt seinen Eltern-Logger: Logger#getParent()
    • Basis-Logger: leerer Name ("")
      • Voreingestelltes Level des Basis-Loggers: Level.INFO (!)
  • Weiterleiten von Nachrichten

    • Nicht verworfene Log-Aufrufe werden an Eltern-Logger weitergeleitet (Default)
      • Abschalten mit Logger#setUseParentHandlers(false);
    • Diese leiten an ihre Handler sowie an ihren Eltern-Logger weiter (unabhängig von Log-Level!)

Wrap-Up

  • Java Logging API im Paket java.util.logging

  • Neuer Logger über Factory-Methode der Klasse Logger

    • Einstellbares Log-Level (Klasse Level)
    • Handler kümmern sich um die Ausgabe, nutzen dazu Formatter
    • Mehrere Handler je Logger registrierbar
    • Log-Level auch für Handler einstellbar (!)
    • Logger (und Handler) "interessieren" sich nur für Meldungen ab bestimmter Wichtigkeit
    • Logger reichen nicht verworfene Meldungen defaultmäßig an Eltern-Logger weiter (rekursiv)
Challenges

Logger-Konfiguration

Betrachten Sie den folgenden Java-Code:

import java.util.logging.*;

public class Logging {
    public static void main(String... args) {
        Logger l = Logger.getLogger("Logging");
        l.setLevel(Level.FINE);

        ConsoleHandler myHandler = new ConsoleHandler();
        myHandler.setFormatter(new SimpleFormatter() {
            public String format(LogRecord record) {
                return "WUPPIE\n";
            }
        });
        l.addHandler(myHandler);

        l.info("A");
        l.fine("B");
        l.finer("C");
        l.finest("D");
        l.severe("E");
    }
}

Welche Ausgaben entstehen durch den obigen Code? Erklären Sie, welche der Logger-Aufrufe zu einer Ausgabe führen und wieso und wie diese Ausgaben zustande kommen bzw. es keine Ausgabe bei einem Logger-Aufruf gibt. Gehen Sie dabei auf jeden der fünf Aufrufe ein.

Analyse eines Live-Beispiels aus dem Dungeon

Analysieren Sie die Konfiguration des Loggers im Dungeon-Projekt: Dungeon-CampusMinden/Dungeon: core/utils/logging/LoggerConfig.java.

Quellen

Code Smells

TL;DR

Code entsteht nicht zum Selbstzweck, er muss von anderen Menschen leicht verstanden und gewartet werden können: Entwickler verbringen einen wesentlichen Teil ihrer Zeit mit dem Lesen von (fremdem) Code. Dabei helfen "Coding Conventions", die eine gewisse einheitliche äußerliche Erscheinung des Codes vorgeben (Namen, Einrückungen, ...). Die Beachtung von grundlegenden Programmierprinzipien hilft ebenso, die Lesbarkeit und Verständlichkeit zu verbessern.

Code, der diese Konventionen und Regeln verletzt, zeigt sogenannte "Code Smells" oder "Bad Smells". Das sind Probleme im Code, die noch nicht direkt zu einem Fehler führen, die aber im Laufe der Zeit die Chance für echte Probleme deutlich erhöht.

Videos (YouTube)
Videos (HSBI-Medienportal)
Lernziele
  • (K3) Erkennen und Vermeiden von Code Smells
  • (K3) Unterscheiden von leicht lesbarem und schwer lesbarem Code
  • (K3) Programmierprinzipien anwenden, um den Code sauberer zu gestalten
  • (K3) Bessere Kommentare schreiben

Code Smells: Ist das Code oder kann das weg?

class checker {
    static public void CheckANDDO(DATA1 inp, int c, FH.Studi
    CustD, int x, int y, int in, int out,int c1, int c2, int c3 = 4)
{
    public int i; // neues i
for(i=0;i<10;i++) // fuer alle i
{
        inp.kurs[0] = 10; inp.kurs[i] = CustD.cred[i]/c;
}
      SetDataToPlan(  CustD  );
    public double myI = in*2.5; // myI=in*2.5
    if (c1)
        out = myI; //OK
    else if(  c3 == 4  )
    {
        myI = c2 * myI;
    if (c3 != 4 || true ) { // unbedingt beachten!
        //System.out.println("x:"+(x++));
        System.out.println("x:"+(x++)); // x++
        System.out.println("out: "+out);
    } }}   }

Der Code im obigen Beispiel lässt sich möglicherweise kompilieren. Und möglicherweise tut er sogar das, was er tun soll.

Dennoch: Der Code "stinkt" (zeigt Code Smells):

  • Nichtbeachtung üblicher Konventionen (Coding Rules)
  • Schlechte Kommentare
  • Auskommentierter Code
  • Fehlende Datenkapselung
  • Zweifelhafte Namen
  • Duplizierter Code
  • "Langer" Code: Lange Methoden, Klassen, Parameterlisten, tief verschachtelte if/then-Bedingungen, ...
  • Feature Neid
  • switch/case oder if/else statt Polymorphie
  • Globale Variablen, lokale Variablen als Attribut
  • Magic Numbers

Diese Liste enthält die häufigsten "Smells" und ließe sich noch beliebig fortsetzen. Schauen Sie mal in die unten angegebene Literatur :-)

Stinkender Code führt zu möglichen (späteren) Problemen.

Was ist guter ("sauberer") Code ("Clean Code")?

Im Grunde bezeichnet "sauberer Code" ("Clean Code") die Abwesenheit von Smells. D.h. man könnte Code als "sauberen" Code bezeichnen, wenn die folgenden Eigenschaften erfüllt sind (keine vollständige Aufzählung!):

  • Gut ("angenehm") lesbar
  • Schnell verständlich: Geeignete Abstraktionen
  • Konzentriert sich auf eine Aufgabe
  • So einfach und direkt wie möglich
  • Ist gut getestet

In [Martin2009] lässt der Autor Robert Martin verschiedene Ikonen der SW-Entwicklung zu diesem Thema zu Wort kommen - eine sehr lesenswerte Lektüre!

=> Jemand kümmert sich um den Code; solides Handwerk

Warum ist guter ("sauberer") Code so wichtig?

Any fool can write code that a computer can understand. Good programmers write code that humans can understand.

 Quelle: [Fowler2011, p. 15]

Auch wenn das zunächst seltsam klingt, aber Code muss auch von Menschen gelesen und verstanden werden können. Klar, der Code muss inhaltlich korrekt sein und die jeweilige Aufgabe erfüllen, er muss kompilieren etc. ... aber er muss auch von anderen Personen weiter entwickelt werden und dazu gelesen und verstanden werden. Guter Code ist nicht einfach nur inhaltlich korrekt, sondern kann auch einfach verstanden werden.

Code, der nicht einfach lesbar ist oder nur schwer verständlich ist, wird oft in der Praxis später nicht gut gepflegt: Andere Entwickler haben (die berechtigte) Angst, etwas kaputt zu machen und arbeiten "um den Code herum". Nur leider wird das Konstrukt dann nur noch schwerer verständlich ...

Code Smells

Verstöße gegen die Prinzipien von Clean Code nennt man auch Code Smells: Der Code "stinkt" gewissermaßen. Dies bedeutet nicht unbedingt, dass der Code nicht funktioniert (d.h. er kann dennoch compilieren und die Anforderungen erfüllen). Er ist nur nicht sauber formuliert, schwer verständlich, enthält Doppelungen etc., was im Laufe der Zeit die Chance für tatsächliche Probleme deutlich erhöht.

Und weil es so wichtig ist, hier gleich noch einmal:

Stinkender Code führt zu möglichen (späteren) Problemen.

"Broken Windows" Phänomen

Wenn ein Gebäude leer steht, wird es eine gewisse Zeit lang nur relativ langsam verfallen: Die Fenster werden nicht mehr geputzt, es sammelt sich Graffiti, Gras wächst in der Dachrinne, Putz blättert ab ...

Irgendwann wird dann eine Scheibe eingeworfen. Wenn dieser Punkt überschritten ist, beschleunigt sich der Verfall rasant: Über Nacht werden alle erreichbaren Scheiben eingeworfen, Türen werden zerstört, es werden sogar Brände gelegt ...

Das passiert auch bei Software! Wenn man als Entwickler das Gefühl bekommt, die Software ist nicht gepflegt, wird man selbst auch nur relativ schlechte Arbeit abliefern. Sei es, weil man nicht versteht, was der Code macht und sich nicht an die Überarbeitung der richtigen Stellen traut und stattdessen die Änderungen als weiteren "Erker" einfach dran pappt. Seit es, weil man keine Lust hat, Zeit in ordentliche Arbeit zu investieren, weil der Code ja eh schon schlecht ist ... Das wird mit der Zeit nicht besser ...

Maßeinheit für Code-Qualität ;-)

Es gibt eine "praxisnahe" (und nicht ganz ernst gemeinte) Maßeinheit für Code-Qualität: Die "WTF/m" (What the Fuck per minute): Thom Holwerda: www.osnews.com/story/19266/WTFs_.

Wenn beim Code-Review durch Kollegen viele "WTF" kommen, ist der Code offenbar nicht in Ordnung ...

Code Smells: Nichtbeachtung von Coding Conventions

  • Richtlinien für einheitliches Aussehen => Andere Programmierer sollen Code schnell lesen können

    • Namen, Schreibweisen
    • Kommentare (Ort, Form, Inhalt)
    • Einrückungen und Spaces vs. Tabs
    • Zeilenlängen, Leerzeilen
    • Klammern
  • Beispiele: Sun Code Conventions, Google Java Style

  • Hinweis: Betrifft vor allem die (äußere) Form!

Code Smells: Schlechte Kommentare I

  • Ratlose Kommentare

    /* k.A. was das bedeutet, aber wenn man es raus nimmt, geht's nicht mehr */
    /* TODO: was passiert hier, und warum? */

    Der Programmierer hat selbst nicht verstanden (und macht sich auch nicht die Mühe zu verstehen), was er da tut! Fehler sind vorprogrammiert!

  • Redundante Kommentare: Erklären Sie, was der Code inhaltlich tun sollte (und warum)!

    public int i; // neues i
    for(i=0;i<10;i++)
    // fuer alle i

    Was würden Sie Ihrem Kollegen erklären (müssen), wenn Sie ihm/ihr den Code vorstellen?

    Wiederholen Sie nicht, was der Code tut (das kann ich ja selbst lesen), sondern beschreiben Sie, was der Code tun sollte und warum.

    Beschreiben Sie dabei auch das Konzept hinter einem Codebaustein.

Code Smells: Schlechte Kommentare II

  • Veraltete Kommentare

    Hinweis auf unsauberes Arbeiten: Oft wird im Zuge der Überarbeitung von Code-Stellen vergessen, auch den Kommentar anzupassen! Sollte beim Lesen extrem misstrauisch machen.

  • Auskommentierter Code

    Da ist jemand seiner Sache unsicher bzw. hat eine Überarbeitung nicht abgeschlossen. Die Chance, dass sich der restliche Code im Laufe der Zeit so verändert, dass der auskommentierte Code nicht mehr (richtig) läuft, ist groß! Auskommentierter Code ist gefährlich und dank Versionskontrolle absolut überflüssig!

  • Kommentare erscheinen zwingend nötig

    Häufig ein Hinweis auf ungeeignete Wahl der Namen (Klassen, Methoden, Attribute) und/oder auf ein ungeeignetes Abstraktionsniveau (beispielsweise Nichtbeachtung des Prinzips der "Single Responsibility")!

    Der Code soll im Normalfall für sich selbst sprechen: WAS wird gemacht. Der Kommentar erklärt im Normalfall, WARUM der Code das machen soll.

  • Unangemessene Information, z.B. Änderungshistorien

    Hinweise wie "wer hat wann was geändert" gehören in das Versionskontroll- oder ins Issue-Tracking-System. Die Änderung ist im Code sowieso nicht mehr sichtbar/nachvollziehbar!

Code Smells: Schlechte Namen und fehlende Kapselung

public class Studi extends Person {
    public String n;
    public int c;

    public void prtIf() { ... }
}

Nach drei Wochen fragen Sie sich, was n oder c oder Studi#prtIf() wohl sein könnte! (Ein anderer Programmierer fragt sich das schon beim ersten Lesen.) Klassen und Methoden sollten sich erwartungsgemäß verhalten.

Wenn Dinge öffentlich angeboten werden, muss man damit rechnen, dass andere darauf zugreifen. D.h. man kann nicht mehr so einfach Dinge wie die interne Repräsentation oder die Art der Berechnung austauschen! Öffentliche Dinge gehören zur Schnittstelle und damit Teil des "Vertrags" mit den Nutzern!

  • Programmierprinzip "Prinzip der minimalen Verwunderung"

    • Klassen und Methoden sollten sich erwartungsgemäß verhalten
    • Gute Namen ersparen das Lesen der Dokumentation
  • Programmierprinzip "Kapselung/Information Hiding"

    • Möglichst schlanke öffentliche Schnittstelle
    • => "Vertrag" mit Nutzern der Klasse!

Code Smells: Duplizierter Code

public class Studi {
    public String getName() { return name; }
    public String getAddress() {
        return strasse+", "+plz+" "+stadt;
    }

    public String getStudiAsString() {
        return name+" ("+strasse+", "+plz+" "+stadt+")";
    }
}
  • Programmierprinzip "DRY" => "Don't repeat yourself!"

Im Beispiel wird das Formatieren der Adresse mehrfach identisch implementiert, d.h. duplizierter Code. Auslagern in eigene Methode und aufrufen!

Kopierter/duplizierter Code ist problematisch:

  • Spätere Änderungen müssen an mehreren Stellen vorgenommen werden
  • Lesbarkeit/Orientierung im Code wird erschwert (Analogie: Reihenhaussiedlung)
  • Verpasste Gelegenheit für sinnvolle Abstraktion!

Code Smells: Langer Code

  • Lange Klassen

    • Faustregel: 5 Bildschirmseiten sind viel
  • Lange Methoden

    • Faustregel: 1 Bildschirmseite
    • [Martin2009]: deutlich weniger als 20 Zeilen
  • Lange Parameterlisten

    • Faustregel: max. 3 ... 5 Parameter
    • [Martin2009]: 0 Parameter ideal, ab 3 Parameter gute Begründung nötig
  • Tief verschachtelte if/then-Bedingungen

    • Faustregel: 2 ... 3 Einrückungsebenen sind viel
  • Programmierprinzip "Single Responsibility"

    Jede Klasse ist für genau einen Aspekt des Gesamtsystems verantwortlich

Lesbarkeit und Übersichtlichkeit leiden

  • Der Mensch kann sich nur begrenzt viele Dinge im Kurzzeitgedächtnis merken
  • Klassen, die länger als 5 Bildschirmseiten sind, erfordern viel Hin- und Her-Scrollen, dito für lange Methoden
  • Lange Methoden sind schwer verständlich (erledigen viele Dinge?)
  • Mehr als 3 Parameter kann sich kaum jemand merken, vor allem beim Aufruf von Methoden
  • Die Testbarkeit wird bei zu komplexen Methoden/Klassen und vielen Parametern sehr erschwert
  • Große Dateien verleiten (auch mangels Übersichtlichkeit) dazu, neuen Code ebenfalls schluderig zu gliedern

Langer Code deutet auch auf eine Verletzung des Prinzips der Single Responsibility hin

  • Klassen fassen evtl. nicht zusammengehörende Dinge zusammen

    public class Student {
        private String name;
        private String phoneAreaCode;
        private String phoneNumber;
    
        public void printStudentInfo() {
            System.out.println("name:    " + name);
            System.out.println("contact: " + phoneAreaCode + "/" + phoneNumber);
        }
    }

    Warum sollte sich die Klasse Student um die Einzelheiten des Aufbaus einer Telefonnummer kümmern? Das Prinzip der "Single Responsibility" wird hier verletzt!

  • Methoden erledigen vermutlich mehr als nur eine Aufgabe

    public void credits() {
        for (Student s : students) {
            if (s.hasSemesterFinished()) {
                ECTS c = calculateEcts(s);
                s.setEctsSum(c);
            }
        }
    }
    
    // Diese Methode erledigt 4 Dinge: Iteration, Abfrage, Berechnung, Setzen ...

    => Erklären Sie die Methode jemandem. Wenn dabei das Wort "und" vorkommt, macht die Methode höchstwahrscheinlich zu viel!

  • Viele Parameter bedeuten oft fehlende Datenabstraktion

    Circle makeCircle(int x, int y, int radius);
    Circle makeCircle(Point center, int radius);  // besser!

Code Smells: Feature Neid

public class CreditsCalculator {
    public ECTS calculateEcts(Student s) {
        int semester = s.getSemester();
        int workload = s.getCurrentWorkload();
        int nrModuls = s.getNumberOfModuls();
        int total = Math.min(30, workload);
        int extra = Math.max(0, total - 30);
        if (semester < 5) {
             extra = extra * nrModuls;
        }
        return new ECTS(total + extra);
    }
}
  • Zugriff auf (viele) Interna der anderen Klasse! => Hohe Kopplung der Klassen!
  • Methode CreditsCalculator#calculateEcts() "möchte" eigentlich in Student sein ...

Wrap-Up

  • Code entsteht nicht zum Selbstzweck => Lesbarkeit ist wichtig

  • Code Smells: Code führt zu möglichen (späteren) Problemen

    • Richtiges Kommentieren und Dokumentieren

      In dieser Sitzung haben wir vor allem auf Kommentare geschaut. Zum Thema Dokumentieren siehe die Einheit zu “Javadoc”.

    • Einhalten von Coding Conventions

      • Regeln zu Schreibweisen und Layout
      • Leerzeichen, Einrückung, Klammern
      • Zeilenlänge, Umbrüche
      • Kommentare
    • Einhalten von Prinzipien des objektorientierten Programmierens

      • Jede Klasse ist für genau einen Aspekt des Systems verantwortlich. (Single Responsibility)
      • Keine Code-Duplizierung! (DRY - Don't repeat yourself)
      • Klassen und Methoden sollten sich erwartungsgemäß verhalten
      • Kapselung: Möglichst wenig öffentlich zugänglich machen
Quellen
  • [Fowler2011] Refactoring
    Fowler, M., Addison-Wesley, 2011. ISBN 978-0-201-48567-7.
  • [Inden2013] Der Weg zum Java-Profi
    Inden, M., dpunkt.verlag, 2013. ISBN 978-3-8649-1245-0.
    Kapitel 10: Bad Smells
  • [Martin2009] Clean Code
    Martin, R., mitp, 2009. ISBN 978-3-8266-5548-7.
  • [Passig2013] Weniger schlecht programmieren
    Passig, K. und Jander, J., O'Reilly, 2013. ISBN 978-3-89721-567-2.

Coding Conventions und Metriken

TL;DR

Code entsteht nicht zum Selbstzweck, er muss von anderen Menschen leicht verstanden und gewartet werden können: Entwickler verbringen einen wesentlichen Teil ihrer Zeit mit dem Lesen von (fremdem) Code.

Dabei helfen "Coding Conventions", die eine gewisse einheitliche äußerliche Erscheinung des Codes vorgeben (Namen, Einrückungen, ...). Im Java-Umfeld ist der "Google Java Style" bzw. der recht ähnliche "AOSP Java Code Style for Contributors" häufig anzutreffen. Coding Conventions beinhalten typischerweise Regeln zu

  • Schreibweisen und Layout
  • Leerzeichen, Einrückung, Klammern
  • Zeilenlänge, Umbrüche
  • Kommentare

Die Beachtung von grundlegenden Programmierprinzipien hilft ebenso, die Lesbarkeit und Verständlichkeit zu verbessern.

Metriken sind Kennzahlen, die aus dem Code berechnet werden, und können zur Überwachung der Einhaltung von Coding Conventions und anderen Regeln genutzt werden. Nützliche Metriken sind dabei NCSS (Non Commenting Source Statements), McCabe (Cyclomatic Complexity), BEC (Boolean Expression Complexity) und DAC (Class Data Abstraction Coupling).

Für die Formatierung des Codes kann man die IDE nutzen, muss dort dann aber die Regeln detailliert manuell einstellen. Das Tool Spotless lässt sich dagegen in den Build-Prozess einbinden und kann die Konfiguration über ein vordefiniertes Regelset passend zum Google Java Style/AOSP automatisiert vornehmen.

Die Prüfung der Coding Conventions und Metriken kann durch das Tool Checkstyle erfolgen. Dieses kann beispielsweise als Plugin in der IDE oder direkt in den Build-Prozess eingebunden werden und wird mit Hilfe einer XML-Datei konfiguriert.

Um typische Anti-Pattern zu vermeiden, kann man den Code mit sogenannten Lintern prüfen. Ein Beispiel für die Java-Entwicklung ist SpotBugs, welches sich in den Build-Prozess einbinden lässt und über 400 typische problematische Muster im Code erkennt.

Für die Praktika in der Veranstaltung Programmiermethoden wird der Google Java Style oder AOSP genutzt. Für die passende Checkstyle-Konfiguration wird eine minimale checkstyle.xml bereitgestellt (vgl. Folie "Konfiguration für das PM-Praktikum").

Videos (HSBI-Medienportal)
Lernziele
  • (K2) Erklären verschiedener Coding Conventions
  • (K2) Erklären wichtiger Grundregeln des objektorientierten Programmierens
  • (K2) Erklären der Metriken NCSS, McCabe, BEC, DAC
  • (K3) Einhalten der wichtigsten Grundregeln des objektorientierten Programmierens
  • (K3) Einhalten der wichtigsten Coding Conventions (Formatierung, Namen, Metriken)
  • (K3) Nutzung des Tools Spotless (Formatierung des Codes)
  • (K3) Nutzung des Tools Checkstyle (Coding Conventions und Metriken)
  • (K3) Nutzung des Tools SpotBugs (Vermeiden von Anti-Pattern)

Coding Conventions: Richtlinien für einheitliches Aussehen von Code

=> Ziel: Andere Programmierer sollen Code schnell lesen können

  • Namen, Schreibweisen: UpperCamelCase vs. lowerCamelCase vs. UPPER_SNAKE_CASE
  • Kommentare (Ort, Form, Inhalt): Javadoc an allen public und protected Elementen
  • Einrückungen und Spaces vs. Tabs: 4 Spaces
  • Zeilenlängen: 100 Zeichen
  • Leerzeilen: Leerzeilen für Gliederung
  • Klammern: Auf selber Zeile wie Code

Beispiele: Sun Code Conventions, Google Java Style, AOSP Java Code Style for Contributors

Beispiel nach Google Java Style/AOSP formatiert

package wuppie.deeplearning.strategy;

/**
 * Demonstriert den Einsatz von AOSP/Google Java Style ................. Umbruch nach 100 Zeichen |
 */
public class MyWuppieStudi implements Comparable<MyWuppieStudi> {
    private static String lastName;
    private static MyWuppieStudi studi;

    private MyWuppieStudi() {}

    /** Erzeugt ein neues Exemplar der MyWuppieStudi-Spezies (max. 40 Zeilen) */
    public static MyWuppieStudi getMyWuppieStudi(String name) {
        if (studi == null) {
            studi = new MyWuppieStudi();
        }
        if (lastName == null) lastName = name;

        return studi;
    }

    @Override
    public int compareTo(MyWuppieStudi o) {
        return lastName.compareTo(lastName);
    }
}

Dieses Beispiel wurde nach Google Java Style/AOSP formatiert.

Die Zeilenlänge beträgt max. 100 Zeichen. Pro Methode werden max. 40 Zeilen genutzt. Zwischen Attributen, Methoden und Importen wird jeweils eine Leerzeile eingesetzt (zwischen den einzelnen Attributen muss aber keine Leerzeile genutzt werden). Zur logischen Gliederung können innerhalb von Methoden weitere Leerzeilen eingesetzt werden, aber immer nur eine.

Klassennamen sind UpperCamelCase, Attribute und Methoden und Parameter lowerCamelCase, Konstanten (im Beispiel nicht vorhanden) UPPER_SNAKE_CASE. Klassen sind Substantive, Methoden Verben.

Alle public und protected Elemente werden mit einem Javadoc-Kommentar versehen. Überschriebene Methoden müssen nicht mit Javadoc kommentiert werden, müssen aber mit @Override markiert werden.

Geschweifte Klammern starten immer auf der selben Codezeile. Wenn bei einem if nur ein Statement vorhanden ist und dieses auf die selbe Zeile passt, kann auf die umschließenden geschweiften Klammern ausnahmsweise verzichtet werden.

Es wird mit Leerzeichen eingerückt. Google Java Style arbeitet mit 2 Leerzeichen, während AOSP hier 4 Leerzeichen vorschreibt. Im Beispiel wurde nach AOSP eingerückt.

Darüber hinaus gibt es vielfältige weitere Regeln für das Aussehen des Codes. Lesen Sie dazu entsprechend auf Google Java Style und auch auf AOSP nach.

Formatieren Sie Ihren Code (mit der IDE)

Sie können den Code manuell formatieren, oder aber (sinnvollerweise) über Tools formatieren lassen. Hier einige Möglichkeiten:

  • IDE: Code-Style einstellen und zum Formatieren nutzen

  • google-java-format: java -jar google-java-format.jar --replace *.java (auch als IDE-Plugin)

  • Spotless in Gradle:

    plugins {
        id "java"
        id "com.diffplug.spotless" version "6.5.0"
    }
    
    spotless {
        java {
            // googleJavaFormat()
            googleJavaFormat().aosp()  // indent w/ 4 spaces
        }
    }

    Prüfen mit ./gradlew spotlessCheck (Teil von ./gradlew check) und Formatieren mit ./gradlew spotlessApply

Einstellungen der IDE's

  • Eclipse:
    • Project > Properties > Java Code Style > Formatter: Coding-Style einstellen/einrichten
    • Code markieren, Source > Format
    • Komplettes Aufräumen: Source > Clean Up (Formatierung, Importe, Annotationen, ...) Kann auch so eingestellt werden, dass ein "Clean Up" immer beim Speichern ausgeführt wird!
  • IntelliJ verfügt über ähnliche Fähigkeiten:
    • Einstellen über Preferences > Editor > Code Style > Java
    • Formatieren mit Code > Reformat Code oder Code > Reformat File

Die Details kann/muss man einzeln einstellen. Für die "bekannten" Styles (Google Java Style) bringen die IDE's oft aber schon eine Gesamtkonfiguration mit.

Achtung: Zumindest in Eclipse gibt es mehrere Stellen, wo ein Code-Style eingestellt werden kann ("Clean Up", "Formatter", ...). Diese sollten dann jeweils auf den selben Style eingestellt werden, sonst gibt es unter Umständen lustige Effekte, da beim Speichern ein anderer Style angewendet wird als beim "Clean Up" oder beim "Format Source" ...

Analog sollte man bei der Verwendung von Checkstyle auch in der IDE im Formatter die entsprechenden Checkstyle-Regeln (s.u.) passend einstellen, sonst bekommt man durch Checkstyle Warnungen angezeigt, die man durch ein automatisches Formatieren nicht beheben kann.

Google Java Style und google-java-format

Wer direkt den Google Java Style nutzt, kann auch den dazu passenden Formatter von Google einsetzen: google-java-format. Diesen kann man entweder als Plugin für IntelliJ/Eclipse einsetzen oder als Stand-alone-Tool (Kommandozeile oder Build-Skripte) aufrufen. Wenn man sich noch einen entsprechenden Git-Hook definiert, wird vor jedem Commit der Code entsprechend den Richtlinien formatiert :)

Spotless und google-java-format in Gradle

Hinweis: Bei Spotless in Gradle müssen je nach den Versionen von Spotless/google-java-format bzw. des JDK noch Optionen in der Datei gradle.properties eingestellt werden (siehe Demo und Spotless > google-java-format (Web)).

Tipp: Die Formatierung über die IDE ist angenehm, aber in der Praxis leider oft etwas hakelig: Man muss alle Regeln selbst einstellen (und es gibt einige dieser Einstellungen), und gerade IntelliJ "greift" manchmal nicht alle Code-Stellen beim Formatieren. Nutzen Sie Spotless und bauen Sie die Konfiguration in Ihr Build-Skript ein und konfigurieren Sie über den Build-Prozess.

Metriken: Kennzahlen für verschiedene Aspekte zum Code

Metriken messen verschiedene Aspekte zum Code und liefern eine Zahl zurück. Mit Metriken kann man beispielsweise die Einhaltung der Coding Rules (Formate, ...) prüfen, aber auch die Einhaltung verschiedener Regeln des objektorientierten Programmierens.

Beispiele für wichtige Metriken (jeweils Max-Werte für PM)

Die folgenden Metriken und deren Maximal-Werte sind gute Erfahrungswerte aus der Praxis und helfen, den Code Smell "Langer Code" (vgl. “Code Smells”) zu erkennen und damit zu vermeiden. Über die Metriken BEC, McCabe und DAC wird auch die Einhaltung elementarer Programmierregeln gemessen.

  • NCSS (Non Commenting Source Statements)
    • Zeilen pro Methode: 40; pro Klasse: 250; pro Datei: 300 Annahme: Eine Anweisung je Zeile ...
  • Anzahl der Methoden pro Klasse: 10
  • Parameter pro Methode: 3
  • BEC (Boolean Expression Complexity) Anzahl boolescher Ausdrücke in if etc.: 3
  • McCabe (Cyclomatic Complexity)
    • Anzahl der möglichen Verzweigungen (Pfade) pro Methode + 1
    • 1-4 gut, 5-7 noch OK
  • DAC (Class Data Abstraction Coupling)
    • Anzahl der genutzten (instantiierten) "Fremdklassen"
    • Werte kleiner 7 werden i.A. als normal betrachtet

Die obigen Grenzwerte sind typische Standardwerte, die sich in der Praxis allgemein bewährt haben (vergleiche u.a. [Martin2009] oder auch in AOSP: Write short methods und AOSP: Limit line length).

Dennoch sind das keine absoluten Werte an sich. Ein Übertreten der Grenzen ist ein Hinweis darauf, dass höchstwahrscheinlich etwas nicht stimmt, muss aber im konkreten Fall hinterfragt und diskutiert und begründet werden!

Metriken im Beispiel von oben

    private static String lastName;
    private static MyWuppieStudi studi;

    public static MyWuppieStudi getMyWuppieStudi(String name) {
        if (studi == null) {
            studi = new MyWuppieStudi();
        }
        if (lastName == null) lastName = name;

        return studi;
    }
  • BEC: 1 (nur ein boolescher Ausdruck im if)
  • McCabe: 3 (es gibt zwei mögliche Verzweigungen in der Methode plus die Methode selbst)
  • DAC: 1 (eine "Fremdklasse": String)

Anmerkung: In Checkstyle werden für einige häufig verwendete Standard-Klassen Ausnahmen definiert, d.h. String würde im obigen Beispiel nicht bei DAC mitgezählt/angezeigt.

=> Verweis auf LV Softwareengineering

Tool-Support: Checkstyle

Metriken und die Einhaltung von Coding-Conventions werden sinnvollerweise nicht manuell, sondern durch diverse Tools erfasst, etwa im Java-Bereich mit Hilfe von Checkstyle.

Das Tool lässt sich Standalone über CLI nutzen oder als Plugin für IDE's (Eclipse oder IntelliJ) einsetzen. Gradle bringt ein eigenes Plugin mit.

  • IDE: diverse Plugins: Eclipse-CS, CheckStyle-IDEA

  • CLI: java -jar checkstyle-10.2-all.jar -c google_checks.xml *.java

  • Plugin "checkstyle" in Gradle:

    plugins {
        id "java"
        id "checkstyle"
    }
    
    checkstyle {
        configFile file('checkstyle.xml')
        toolVersion '10.2'
    }
    • Aufruf: Prüfen mit ./gradlew checkstyleMain (Teil von ./gradlew check)
    • Konfiguration: <projectDir>/config/checkstyle/checkstyle.xml (Default) bzw. mit der obigen Konfiguration direkt im Projektordner
    • Report: <projectDir>/build/reports/checkstyle/main.html

Checkstyle: Konfiguration

Die auszuführenden Checks lassen sich über eine XML-Datei konfigurieren. In Eclipse-CS kann man die Konfiguration auch in einer GUI bearbeiten.

Das Checkstyle-Projekt stellt eine passende Konfiguration für den Google Java Style bereit. Diese ist auch in den entsprechenden Plugins oft bereits enthalten und kann direkt ausgewählt oder als Startpunkt für eigene Konfigurationen genutzt werden.

Der Startpunkt für die Konfigurationsdatei ist immer das Modul "Checker". Darin können sich "FileSetChecks" (Module, die auf einer Menge von Dateien Checks ausführen), "Filters" (Module, die Events bei der Prüfung von Regeln filtern) und "AuditListeners" (Module, die akzeptierte Events in einen Report überführen) befinden. Der "TreeWalker" ist mit der wichtigste Vertreter der FileSetChecks-Module und transformiert die zu prüfenden Java-Sourcen in einen Abstract Syntax Tree, also eine Baumstruktur, die dem jeweiligen Code unter der Java-Grammatik entspricht. Darauf können dann wiederum die meisten Low-Level-Module arbeiten.

Eine Reihe von Standard-Checks sind bereits in Checkstyle implementiert und benötigen keine weitere externe Abhängigkeiten. Man kann aber zusätzliche Regeln aus anderen Projekten beziehen (etwa via Gradle/Maven) oder sich eigene zusätzliche Regeln in Java schreiben. Die einzelnen Checks werden in der Regel als "Modul" dem "TreeWalker" hinzugefügt und über die jeweiligen Properties näher konfiguriert.

Sie finden in der Doku zu jedem Check das entsprechende Modul, das Eltern-Modul (also wo müssen Sie das Modul im XML-Baum einfügen) und auch die möglichen Properties und deren Default-Einstellungen.

<module name="Checker">
    <module name="LineLength">
        <property name="max" value="100"/>
    </module>

    <module name="TreeWalker">
        <module name="AvoidStarImport"/>
        <module name="MethodCount">
            <property name="maxPublic" value="10"/>
            <property name="maxTotal" value="40"/>
        </module>
    </module>
</module>

Alternativen/Ergänzungen: beispielsweise MetricsReloaded.

SpotBugs: Finde Anti-Pattern und potentielle Bugs (Linter)

  • SpotBugs sucht nach über 400 potentiellen Bugs im Code

    • Anti-Pattern (schlechte Praxis, "dodgy" Code)
    • Sicherheitsprobleme
    • Korrektheit
  • CLI: java -jar spotbugs.jar options ...

  • IDE: IntelliJ SpotBugs plugin, SpotBugs Eclipse plugin

  • Gradle: SpotBugs Gradle Plugin

    plugins {
        id "java"
        id "com.github.spotbugs" version "5.0.6"
    }
    spotbugs {
        ignoreFailures = true
        showStackTraces = false
    }

    Prüfen mit ./gradlew spotbugsMain (in ./gradlew check)

Konfiguration für das PM-Praktikum (Format, Metriken, Checkstyle, SpotBugs)

Im PM-Praktikum beachten wir die obigen Coding Conventions und Metriken mit den dort definierten Grenzwerten. Diese sind bereits in der bereit gestellten Minimal-Konfiguration für Checkstyle (s.u.) konfiguriert.

Formatierung

  • Google Java Style/AOSP: Spotless

Zusätzlich wenden wir den Google Java Style an. Statt der dort vorgeschriebenen Einrückung mit 2 Leerzeichen (und 4+ Leerzeichen bei Zeilenumbruch in einem Statement) können Sie auch mit 4 Leerzeichen einrücken (8 Leerzeichen bei Zeilenumbruch) (AOSP). Halten Sie sich in Ihrem Team an eine einheitliche Einrückung (Google Java Style oder AOSP).

Formatieren Sie Ihren Code vor den Commits mit Spotless (über Gradle) oder stellen Sie den Formatter Ihrer IDE entsprechend ein.

Checkstyle

  • Minimal-Konfiguration für Checkstyle (Coding Conventions, Metriken)

Nutzen Sie die folgende Minimal-Konfiguration für Checkstyle für Ihre Praktikumsaufgaben. Diese beinhaltet die Prüfung der wichtigsten Formate nach Google Java Style/AOSP sowie der obigen Metriken. Halten Sie diese Regeln ein.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE module PUBLIC "-//Checkstyle//DTD Checkstyle Configuration 1.3//EN" "https://checkstyle.org/dtds/configuration_1_3.dtd">

<module name="Checker">
  <property name="severity" value="warning"/>

  <module name="TreeWalker">
    <module name="JavaNCSS">
      <property name="methodMaximum" value="40"/>
      <property name="classMaximum" value="250"/>
      <property name="fileMaximum" value="300"/>
    </module>
    <module name="BooleanExpressionComplexity"/>
    <module name="CyclomaticComplexity">
      <property name="max" value="7"/>
    </module>
    <module name="ClassDataAbstractionCoupling">
      <property name="max" value="6"/>
    </module>
    <module name="MethodCount">
      <property name="maxTotal" value="10"/>
      <property name="maxPrivate" value="10"/>
      <property name="maxPackage" value="10"/>
      <property name="maxProtected" value="10"/>
      <property name="maxPublic" value="10"/>
    </module>
    <module name="ParameterNumber">
      <property name="max" value="3"/>
    </module>
    <module name="MethodLength">
      <property name="max" value="40"/>
    </module>
    <module name="Indentation">
      <property name="basicOffset" value="4"/>
      <property name="lineWrappingIndentation" value="8"/>
      <property name="caseIndent" value="4"/>
      <property name="throwsIndent" value="4"/>
      <property name="arrayInitIndent" value="4"/>
    </module>
    <module name="TypeName"/>
    <module name="MethodName"/>
    <module name="MemberName"/>
    <module name="ParameterName"/>
    <module name="ConstantName"/>
    <module name="OneStatementPerLine"/>
    <module name="MultipleVariableDeclarations"/>
    <module name="MissingOverride"/>
    <module name="MissingJavadocMethod"/>
    <module name="AvoidStarImport"/>
  </module>

  <module name="LineLength">
    <property name="max" value="100"/>
  </module>
  <module name="FileTabCharacter">
    <property name="eachLine" value="true"/>
  </module>
  <module name="NewlineAtEndOfFile"/>
</module>

Sie können diese Basis-Einstellungen auch aus dem Programmiermethoden-CampusMinden/Prog2-Lecture-Repo direkt herunterladen: checkstyle.xml.

Sie können zusätzlich gern noch die weiteren (und strengeren) Regeln aus der vom Checkstyle-Projekt bereitgestellten Konfigurationsdatei für den Google Java Style nutzen. Hinweis: Einige der dort konfigurierten Checkstyle-Regeln gehen allerdings über den Google Java Style hinaus.

Linter: SpotBugs

  • Vermeiden von Anti-Pattern mit SpotBugs

Setzen Sie zusätzlich SpotBugs mit ein. Ihre Lösungen dürfen keine Warnungen oder Fehler beinhalten, die SpotBugs melden würde.

Wrap-Up

  • Code entsteht nicht zum Selbstzweck => Regeln nötig!

    • Coding Conventions

      • Regeln zu Schreibweisen und Layout
      • Leerzeichen, Einrückung, Klammern
      • Zeilenlänge, Umbrüche
      • Kommentare
    • Formatieren mit Spotless

    • Prinzipien des objektorientierten Programmierens (vgl. “Code Smells”)

      • Jede Klasse ist für genau einen Aspekt des Systems verantwortlich. (Single Responsibility)
      • Keine Code-Duplizierung! (DRY - Don't repeat yourself)
      • Klassen und Methoden sollten sich erwartungsgemäß verhalten
      • Kapselung: Möglichst wenig öffentlich zugänglich machen
  • Metriken: Einhaltung von Regeln in Zahlen ausdrücken

  • Prüfung manuell durch Code Reviews oder durch Tools wie Checkstyle oder SpotBugs

  • Definition des "PM-Styles" (siehe Folie "Konfiguration für das PM-Praktikum")

Quellen

Refactoring

TL;DR

Refactoring bedeutet Änderung der inneren Struktur des Codes ohne Beeinflussung äußeren Verhaltens.

Mit Hilfe von Refactoring kann man Code Smells beheben, und Lesbarkeit, Verständlichkeit und Wartbarkeit von Software verbessern.

Es ist wichtig, immer nur einzelne Schritte zu machen und anschließend die Testsuite laufen zu lassen, damit nicht versehentlich Fehler oder Verhaltensänderungen beim Refactoring eingebaut werden.

Prinzipiell kann man Refactoring manuell mit Search&Replace durchführen, aber es bietet sich an, hier die IDE-Unterstützung zu nutzen. Es stehen verschiedene Methoden zur Verfügung, die nicht unbedingt einheitlich benannt sein müssen oder in jeder IDE vorkommen. Zu den häufig genutzten Methoden zählen Rename, Extract, Move und Push Up/Pull Down.

Videos (HSBI-Medienportal)
Lernziele
  • (K2) Begriff, Notwendigkeit und Vorgehen des/beim Refactoring
  • (K2) Bedeutung kleiner Schritte beim Refactoring
  • (K2) Bedeutung einer sinnvollen Testsuite beim Refactoring
  • (K2) Refactoring: Nur innere Struktur ändern, nicht äußeres Verhalten!
  • (K3) Anwendung der wichtigsten Refactoring-Methoden: Rename, Extract, Move, Push Up/Pull Down

Was ist Refactoring?

Refactoring ist, wenn einem auffällt, daß der Funktionsname foobar ziemlich bescheuert ist, und man die Funktion in sinus umbenennt.

 Quelle: "356: Refactoring" by Andreas Bogk on Lutz Donnerhacke: "Fachbegriffe der Informatik"

Refactoring (noun): a change made to the internal structure of software to make it easier to understand and cheaper to modify without changing its observable behaviour.

 Quelle: [Fowler2011, p. 53]

Refactoring: Änderungen an der inneren Struktur einer Software

  • Beobachtbares (äußeres) Verhalten ändert sich dabei nicht
    • Keine neuen Features einführen
    • Keine Bugs fixen
    • Keine öffentliche Schnittstelle ändern (Anmerkung: Bis auf Umbenennungen oder Verschiebungen von Elementen innerhalb der Software)
  • Ziel: Verbesserung von Verständlichkeit und Änderbarkeit

Anzeichen, dass Refactoring jetzt eine gute Idee wäre

  • Code "stinkt" (zeigt/enthält Code Smells)

    Code Smells sind strukturelle Probleme, die im Laufe der Zeit zu Problemen führen können. Refactoring ändert die innere Struktur des Codes und kann entsprechend genutzt werden, um die Smells zu beheben.

  • Schwer erklärbarer Code

    Könnten Sie Ihren Code ohne Vorbereitung in der Abgabe erklären? In einer Minute? In fünf Minuten? In zehn? Gar nicht?

    In den letzten beiden Fällen sollten Sie definitiv über eine Vereinfachung der Strukturen nachdenken.

  • Verständnisprobleme, Erweiterungen

    Sie grübeln in der Abgabe, was Ihr Code machen sollte?

    Sie überlegen, was Ihr Code bedeutet, um herauszufinden, wo Sie die neue Funktionalität anbauen können?

    Sie suchen nach Codeteilen, finden diese aber nicht, da die sich in anderen (falschen?) Stellen/Klassen befinden?

    Nutzen Sie die (neuen) Erkenntnisse, um den Code leichter verständlich zu gestalten.

"Three strikes and you refactor."

 Quelle: [Fowler2011, p. 58]: "The Rule of Three"

Wenn Sie sich zum dritten Mal über eine suboptimale Lösung ärgern, dann werden Sie sich vermutlich noch öfter darüber ärgern. Jetzt ist der Zeitpunkt für eine Verbesserung.

Schauen Sie sich die entsprechenden Kapitel in [Passig2013] und [Fowler2011] an, dort finden Sie noch viele weitere Anhaltspunkte, ob und wann Refactoring sinnvoll ist.

Bevor Sie loslegen ...

  1. Unit Tests schreiben

    • Normale und ungültige Eingaben
    • Rand- und Spezialfälle
  2. Coding Conventions einhalten

    • Sourcecode formatieren (lassen)
  3. Haben Sie die fragliche Codestelle auch wirklich verstanden?!

Vorgehen beim Refactoring

Überblick über die Methoden des Refactorings

Die Refactoring-Methoden sind nicht einheitlich definiert, es existiert ein großer und uneinheitlicher "Katalog" an möglichen Schritten. Teilweise benennt jede IDE die Schritte etwas anders, teilweise werden unterschiedliche Möglichkeiten angeboten.

Zu den am häufigsten genutzten Methoden zählen

  • Rename Method/Class/Field
  • Encapsulate Field
  • Extract Method/Class
  • Move Method
  • Pull Up, Push Down (Field, Method)

Best Practice

Eine Best Practice (oder nennen Sie es einfach eine wichtige Erfahrung) ist, beim Refactoring langsam und gründlich vorzugehen. Sie ändern die Struktur der Software und können dabei leicht Fehler oder echte Probleme einbauen. Gehen Sie also langsam und sorgsam vor, machen Sie einen Schritt nach dem anderen und sichern Sie sich durch eine gute Testsuite ab, die Sie nach jedem Schritt erneut ausführen: Das Verhalten der Software soll sich ja nicht ändern, d.h. die Tests müssen nach jedem einzelnen Refactoring-Schritt immer grün sein (oder Sie haben einen Fehler gemacht).

  • Kleine Schritte: immer nur eine Änderung zu einer Zeit

  • Nach jedem Refactoring-Schritt Testsuite laufen lassen

    => Nächster Refactoring-Schritt erst, wenn alle Tests wieder "grün"

  • Versionskontrolle nutzen: Jeden Schritt einzeln committen

Refactoring-Methode: Rename Method/Class/Field

Motivation

Name einer Methode/Klasse/Attributs erklärt nicht ihren Zweck.

Durchführung

Name selektieren, "Refactor > Rename"

Anschließend ggf. prüfen

Aufrufer? Superklassen?

Beispiel

Vorher

public String getTeN() {}

Nachher

public String getTelefonNummer() {}

Refactoring-Methode: Encapsulate Field

Motivation

Sichtbarkeit von Attributen reduzieren.

Durchführung

Attribut selektieren, "Refactor > Encapsulate Field"

Anschließend ggf. prüfen

Superklassen? Referenzen? (Neue) JUnit-Tests?

Beispiel

Vorher

int cps;

public void printDetails() {
    System.out.println("Credits: " + cps);
}

Nachher

private int cps;

int getCps() { return cps; }
void setCps(int cps) {  this.cps = cps;  }

public void printDetails() {
    System.out.println("credits: " + getCps());
}

Refactoring-Methode: Extract Method/Class

Motivation

  • Codefragment stellt eigenständige Methode dar
  • "Überschriften-Code"
  • Code-Duplizierung
  • Code ist zu "groß"
  • Klasse oder Methode erfüllt unterschiedliche Aufgaben

Durchführung

Codefragment selektieren, "Refactor > Extract Method" bzw. "Refactor > Extract Class"

Anschließend ggf. prüfen

  • Aufruf der neuen Methode? Nutzung der neuen Klasse?
  • Neue JUnit-Tests nötig? Veränderung bestehender Tests nötig?
  • Speziell bei Methoden:
    • Nutzung lokaler Variablen: Übergabe als Parameter!
    • Veränderung lokaler Variablen: Rückgabewert in neuer Methode und Zuweisung bei Aufruf; evtl. neue Typen nötig!

Beispiel

Vorher

public void printInfos() {
    printHeader();
    // Details ausgeben
    System.out.println("name:    " + name);
    System.out.println("credits: " + cps);
}

Nachher

public void printInfos() {
    printHeader();
    printDetails();
}
private void printDetails() {
    System.out.println("name:    " + name);
    System.out.println("credits: " + cps);
}

Refactoring-Methode: Move Method

Motivation

Methode nutzt (oder wird genutzt von) mehr Eigenschaften einer fremden Klasse als der eigenen Klasse.

Durchführung

Methode selektieren, "Refactor > Move" (ggf. "Keep original method as delegate to moved method" aktivieren)

Anschließend ggf. prüfen

  • Aufruf der neuen Methode (Delegation)?
  • Neue JUnit-Tests nötig? Veränderung bestehender Tests nötig?
  • Nutzung lokaler Variablen: Übergabe als Parameter!
  • Veränderung lokaler Variablen: Rückgabewert in neuer Methode und Zuweisung bei Aufruf; evtl. neue Typen nötig!

Beispiel

Vorher

public class Kurs {
    int cps;
    String descr;
}

public class Studi extends Person {
    String name;
    int cps;
    Kurs kurs;

    public void printKursInfos() {
        System.out.println("Kurs:    " + kurs.descr);
        System.out.println("Credits: " + kurs.cps);
    }
}

Nachher

public class Kurs {
    int cps;
    String descr;

    public void printKursInfos() {
        System.out.println("Kurs:    " + descr);
        System.out.println("Credits: " + cps);
    }
}

public class Studi extends Person {
    String name;
    int cps;
    Kurs kurs;

    public void printKursInfos() { kurs.printKursInfos(); }
}

Refactoring-Methode: Pull Up, Push Down (Field, Method)

Motivation

  • Attribut/Methode nur für die Oberklasse relevant: Pull Up
  • Subklassen haben identische Attribute/Methoden: Pull Up
  • Attribut/Methode nur für eine Subklasse relevant: Push Down

Durchführung

Name selektieren, "Refactor > Pull Up" oder "Refactor > Push Down"

Anschließend ggf. prüfen

Referenzen/Aufrufer? JUnit-Tests?

Beispiel

Vorher

public class Person { }

public class Studi extends Person {
    String name;
    public void printDetails() { System.out.println("name:    " + name); }
}

Nachher

public class Person { protected String name; }

public class Studi extends Person {
    public void printDetails() { System.out.println("name:    " + name); }
}

Wrap-Up

Behebung von Bad Smells durch Refactoring

=> Änderung der inneren Struktur ohne Beeinflussung des äußeren Verhaltens

  • Verbessert Lesbarkeit, Verständlichkeit, Wartbarkeit
  • Immer nur kleine Schritte machen
  • Nach jedem Schritt Testsuite laufen lassen
  • Katalog von Maßnahmen, beispielsweise Rename, Extract, Move, Push Up/Pull Down, ...
  • Unterstützung durch IDEs wie Eclipse, Idea, ...
Challenges

Betrachten Sie das Theatrical Players Refactoring Kata. Dort finden Sie im Unterordner java/ einige Klassen mit unübersichtlichem und schlecht strukturierten Code.

Welche Bad Smells können Sie hier identifizieren?

Beheben Sie die Smells durch die schrittweise Anwendung von den aus der Vorlesung bekannten Refactoring-Methoden. Denken Sie auch daran, dass Refactoring immer durch eine entsprechende Testsuite abgesichert sein muss - ergänzen Sie ggf. die Testfälle.

Quellen
  • [Fowler2011] Refactoring
    Fowler, M., Addison-Wesley, 2011. ISBN 978-0-201-48567-7.
  • [Inden2013] Der Weg zum Java-Profi
    Inden, M., dpunkt.verlag, 2013. ISBN 978-3-8649-1245-0.
    Kapitel 11: Refactorings
  • [Passig2013] Weniger schlecht programmieren
    Passig, K. und Jander, J., O'Reilly, 2013. ISBN 978-3-89721-567-2.