Don’t Repeat Yourself (DRY) – Wissenshäppchen #1

Dieser Beitrag ist Teil 1 von 8 in der Serie Wissenshäppchen.

In der ersten Episode meiner „Wissenshäppchen“ widme ich mich einem der wichtigsten Prinzipien der Softwareentwicklung: Don’t Repeat Yourself (DRY). Doppelter Code ist der Feind jedes Entwicklers! 🙂

Every piece of knowledge must have a single, unambiguous, authoritative representation within a system. (DontRepeatYourself)

Am Beispiel einer weit verbreiteten Programmierübung zeige ich den Weg von doppeltem zu „trockenem“ (DRY) Code.

Probeabo bei Audible (Affiliate)

Inhalt

  • Doppelter Code ist ein Code Smell.
  • Er tritt meistens auf, wenn Entwickler Zeit sparen wollen und mit Copy/Paste arbeiten.
  • Doppelter Code führt zu Inkonsistenzen und damit zu Fehlern im Programm.
  • Er äußert sich durch Shotgun Surgery, das Anpassen mehrerer Stellen im Code für die Änderung eines einzigen Features.
  • Es existieren viele Refactorings, die doppelten Code vermeiden sollen.

Die Aufgabe: FizzBuzz

Das hier ist die Beschreibung des zu lösenden Problems:

Print a list of the numbers from 1 to 100 to the console. For numbers that are multiples of 3 print „Fizz“ instead. For numbers that are multiples of 5 print „Buzz“ instead. For numbers that are both multiples of 3 and 5 print „FizzBuzz“ instead. These are the first 15 values the program should print:

1
2
Fizz
4
Buzz
Fizz
7
8
Fizz
Buzz
11
Fizz
13
14
FizzBuzz

Lösung der Azubis

Die Implementierung der Azubis sieht dann meistens so aus:

public static void main(String[] args)
{
    for (int i = 1; i <= 100; i++)
    {
        if (i % 3 == 0 && i % 5 == 0)
        {
            System.out.println("FizzBuzz");
        }
        else
            if (i % 3 == 0)
            {
                System.out.println("Fizz");
            }
            else
                if (i % 5 == 0)
                {
                    System.out.println("Buzz");
                }
                else
                {
                    System.out.println(i);
                }
    }
}

Diese Implementierung ist recht komplex (drei verschachtelte if-Statements) und enthält auch sehr viel doppelten Code:

  • Die auszugebenden Strings. Würden wir das Spiel auf Deutsch übersetzen, müssten wir die Strings an mehreren Stellen verändern.
  • Die Prüfung auf Fizz und Buzz (Modulo-Rechnung). Würden sich die Regeln ändern (z.B. 7 und 11 statt 3 und 5 oder zusätzlich Fizz bei „enthält die Ziffer 3“), müssten sie an mehreren Stellen angepasst werden.
  • Die Ausgabe auf der Konsole. Soll das Spiel in einer Webanwendung oder einer Windows-Applikation eingesetzt werden, müsste die Ausgabe an mehreren Stellen korrigiert werden.

Refactorings

Um die Komplexität und den doppelten Code zu entfernen, können verschiedene, relativ einfache Refactorings angewendet werden:

  • Werte in Variablen oder Konstanten auslagern, die nur einmalig definiert werden.
  • Variable für das Ergebnis einführen und diese nur einmalig ausgeben, anstatt jedes Ergebnis separat.
  • Ergebnisse der einzelnen Prüfungen verketten, anstatt doppelt zu prüfen.

Schritt 1: Doppelte Werte in Variablen auslagern

Fizz und Buzz sollen als Wert nur noch einmalig vorkommen. So sieht eine mögliche Lösung aus:

public static void main(String[] args)
{
    String fizz = "Fizz"; // <--- HIER
    String buzz = "Buzz"; // <--- HIER
    for (int i = 1; i <= 100; i++)
    {
        if (i % 3 == 0 && i % 5 == 0)
        {
            System.out.println(fizz + buzz); // <--- HIER
        }
        else
            if (i % 3 == 0)
            {
                System.out.println(fizz); // <--- HIER
            }
            else
                if (i % 5 == 0)
                {
                    System.out.println(buzz); // <--- HIER
                }
                else
                {
                    System.out.println(i);
                }
    }
}

Schritt 2: Variable für Endergebnis einführen

Anstatt viermal die Ausgabe mit System.out.println() durchzuführen, soll das Ergebnis „gesammelt“ und nur einmal ausgegeben werden. Das könnte dann so aussehen:

public static void main(String[] args)
{
    String fizz = "Fizz";
    String buzz = "Buzz";
    for (int i = 1; i <= 100; i++)
    {
        String ergebnis = ""; // <--- HIER
        if (i % 3 == 0 && i % 5 == 0)
        {
            ergebnis = fizz + buzz; // <--- HIER
        }
        else
            if (i % 3 == 0)
            {
                ergebnis = fizz; // <--- HIER
            }
            else
                if (i % 5 == 0)
                {
                    ergebnis = buzz; // <--- HIER
                }
                else
                {
                    ergebnis = "" + i; // <--- HIER
                }
        System.out.println(ergebnis); // <--- HIER
    }
}

Schritt 3: Doppelte Prüfungen entfernen

Die Ergebnisse der beiden Prüfungen können ebenfalls in Variablen gespeichert werden, um sie wiederzuverwenden. Beispiel:

public static void main(String[] args)
{
    String fizz = "Fizz";
    String buzz = "Buzz";
    for (int i = 1; i <= 100; i++)
    {
        String ergebnis = "";
        boolean isFizz = i % 3 == 0; // <--- HIER
        boolean isBuzz = i % 5 == 0; // <--- HIER
        if (isFizz && isBuzz) // <--- HIER
        {
            ergebnis = fizz + buzz;
        }
        else
            if (isFizz) // <--- HIER
            {
                ergebnis = fizz;
            }
            else
                if (isBuzz) // <--- HIER
                {
                    ergebnis = buzz;
                }
                else
                {
                    ergebnis = "" + i;
                }
        System.out.println(ergebnis);
    }
}

Schritt 4: Komplexität reduzieren

Die Komplexität der geschachtelten if-Statements wird zuletzt aufgehoben. Hierfür gibt es kein einfaches Refactoring, sondern man muss die grundsätzliche Struktur des Codes ändern und ein wenig nachdenken, wie man das erreichen könnte. Wichtig hierbei ist der Fokus darauf, alles Doppelte zu eliminieren. Wenn man sich das vor Augen hält, denkt man automatisch in verschiedene Richtungen und kommt (hoffentlich) auf eine mögliche Lösung.

Zunächst macht man sich deutlich, was eigentlich noch doppelt ist: die Kombination der beiden Prüfungen! Das Zutreffen beider Bedingungen ist eigentlich nur ein Sonderfall der beiden einzelnen Prüfungen. Anstatt nach jeder Prüfung das Endergebnis zu überschreiben, muss es einen Weg geben, die Ergebnisse zu kombinieren. Dem könnte man sich wie folgt annähern:

1) Sonderfall if (isFizz && isBuzz) entfernen und Code kompilierbar machen (überflüssiges else entfernen):

if (isFizz)
{
    ergebnis = fizz;
}
if (isBuzz)
{
    ergebnis = buzz; // noch falsch
}
if (false) // noch falsch
{
    ergebnis = "" + i;
}

2) Anstatt bei isBuzz das Ergebnis zu überschreiben, Buzz anhängen:

if (isFizz)
{
    ergebnis = fizz;
}
if (isBuzz)
{
    ergebnis += buzz; // <--- HIER
}
if (false) // noch falsch
{
    ergebnis = "" + i;
}

3) Die falsche Abfrage beim letzten if korrigieren:

if (isFizz)
{
    ergebnis = fizz;
}
if (isBuzz)
{
    ergebnis += buzz;
}
if (!isFizz && !isBuzz) // <--- HIER
{
    ergebnis = "" + i;
}

4) Wenn jetzt noch die doppelte Verwendung von isFizz und isBuzz vermieden werden soll, kann die letzte Bedingung auf ein anderes Kriterium umgestellt werden:

if (isFizz)
{
    ergebnis = fizz;
}
if (isBuzz)
{
    ergebnis += buzz;
}
if (ergebnis.isEmpty()) // <--- HIER
{
    ergebnis = "" + i;
}

Musterlösung

Meine komplett „Musterlösung“ sieht nun so aus:

public class FizzBuzz
{
    public static void main(String[] args)
    {
        final String fizz = "Fizz";
        final String buzz = "Buzz";
        for (int i = 1; i <= 100; i++)
        {
            String ergebnis = "";
            boolean isFizz = i % 3 == 0;
            boolean isBuzz = i % 5 == 0;
            if (isFizz)
            {
                ergebnis += fizz;
            }
            if (isBuzz)
            {
                ergebnis += buzz;
            }
            if (ergebnis.isEmpty())
            {
                ergebnis += "" + i;
            }
            System.out.println(ergebnis);
        }
    }
}

Ein paar Kleinigkeiten wurden noch angepasst. Aus Gründen der besseren Symmetrie wurden alle drei Zuweisungen zu ergebnis auf Konkatenation umgestellt. Außerdem wurden die Strings fizz und buzz als final deklariert, da sich ihre Werte während der Programmausführung nicht ändern werden. Die Prüfungen wurden aus Gründen der besseren Lesbarkeit nicht wieder inline in die if-Statements geschrieben (siehe Inline Temp, sondern die Zwischenvariablen isFizz und isBuzz wurden beibehalten (siehe Extract Variable).

DRY

Damit wurden alle Anforderungen von Don’t Repeat Yourself umgesetzt:

  • Die Strings können an einer einzigen Stelle „übersetzt“ werden, wenn das Spiel auf Deutsch laufen soll. Beispiel: final String fizz = "Fiss";
  • Die Spielregeln können an einer einzigen Stelle angepasst werden. Beispiel: boolean isFizz = i % 3 == 0 || ("" + i).contains("3");
  • Die Ausgabe kann an einer einzigen Stelle angepasst werden. Beispiel: System.err.println(ergebnis);

Literaturempfehlungen

Martin Fowler zeigt in seinem Standardwerk Refactoring: Improving the Design of Existing Code* viele Beispiele für „Code Smells“ (einer davon ist doppelter Code) und Schritt-für-Schritt-Anleitungen für die Refactorings, die diese Probleme beheben können. Eine absolute Leseempfehlung zum Thema DRY.

Martin Fowler - Refactoring: Improving the Design of Existing Code (Affiliate)*

Links

Navigation der SerieYou Ain’t Gonna Need It (YAGNI) – Wissenshäppchen #2 >>
Polyglot Clean Code Developer
About the Author
Ausbildungsleiter für Fachinformatiker Anwendungsentwicklung und Systemintegration, IHK-Prüfer und Hochschuldozent für Programmierung und Software-Engineering.

5 comments on “Don’t Repeat Yourself (DRY) – Wissenshäppchen #1

  1. Peter sagt:

    Gute Erklärung des DRY Prinzips mit anschaulichem Beispiel. Was mir allerdings fehlt ist eine kritische Betrachtung. DRY ist nicht immer das absolute Ziel. Es kann auch hin und wieder von Vorteil sein doppelten Code zu erhalten. Je nach dem, wie unabhängig die Codestellen voneinander sind oder sein sollen. Was man leider nicht immer von Anfang an weiß.

    Beispiel:
    Die Fachabteilung arbeitet nun eine Weile mit der oben entwickelten Lösung. Irgendwann trudelt eine Änderungsanforderung rein, die besagt: „bei Vielfachen von 3 soll jetzt „Fuzz“ ausgegeben werden.“.
    Ok, ist ja ein leichtes, änder ich einfach die Variable fizz = "Fizz" in fuzz = "Fuzz". Teste das Programm und liefere es neu aus.
    Nach einer Weile kommt der Fachbereich zurück und sagt, dass das Programm jetzt aber falsch sei. Bei Vielfachen von 15 würde jetzt „FuzzBuzz“ ausgegeben, das hätte er nicht bestellt. Das muss „FizzBuzz“ bleiben!
    Tja, eine ungewollter Seiteneffekt. Diese Änderung wäre mit der „naiven“ Implementierung des Azubis ohne Probleme durchführbar gewesen.

  2. Stefan Macke sagt:

    Hallo Peter,

    da gebe ich dir recht! Danke für den Hinweis. Ich werde darauf achten, bei den nächsten Wissenshäppchen auch evtl. negative Konsequenzen zu erläutern.

    Ich halte es immer so, dass ich meinen Azubis/Studierenden die DRY-Regel als „Daumenregel“ zum Einstieg in die Programmierung mitgebe. Dass man dann später davon abweichen kann (und manchmal auch muss), ist ein Lerninhalt für die höheren Lehrjahre. 😉

    Viele Grüße!
    Stefan

  3. Simon sagt:

    Meines Erachtens nach, sollte auch die Neuerzeugung von Strings unter das Prinzip DRY fallen. So werden doch in der Musterlösung 200 Strings im besten, und 300 im schlechtesten Fall erzeugt.
    Weiterhin werden stets drei if -Abfragen geführt. Prüft man als erstes die geteilte Eigenschaft, könnte man bei i % 15 == 0, bereits nach dem ersten Durchgang das Ergebnis schreiben. Im besten Fall spart man 66% der Abfragen ein.

    final String fizz = „Fizz“
    final String buzz = „Buzz“;
    StringBuilder ergebnis = new StringBuilder();
    for (int i = 1; i <= 100; i++) {
    boolean isFizz = i % 3 == 0;
    boolean isBuzz = i % 5 == 0;

    if(isFizz && isBuzz) ergebnis.append(fizz).append(buzz);
    else if (isFizz) ergebnis.append(fizz);
    else if (isBuzz) ergebnis.append(buzz);
    else ergebnis.append(i);
    ergebnis.append("\n");

    }
    System.out.println(ergebnis);

    Hier ein etwas kompakterer Ansatz:
    final String fizz = „Fizz“
    final String buzz = „Buzz“;
    final String fizzbuzz = fizz + buzz;

    StringBuilder sb = new StringBuilder();
    for (int i = 1; i <= 100; i++) {
    sb.append(i % 15 == 0 ? fizzbuzz : i % 3 == 0 ? fizz : i % 5 == 0 ? buzz : i).append(„\n“);
    }
    System.out.println(sb);

    Die vom Compiler entsprechend vorgenommene Umwandlung von String in StringBuilder und zurück, fordert einige Rechnenzeit:
    String ergebnis = „“; // new String(„“);
    ergebnis += fizz; // new StringBuilder().append(ergebnis).append(fizz).toString();
    ergebnis += buzz // new StringBuilder().append(ergebnis).append(buzz).toString();
    ergebnis += „“ + i // new StringBuilder().append(ergebnis).append((new StringBuilder().append(„“).append(i)).toString()).toString();

    Es ist leicht erkennbar, um wie viel aufwendiger das Ganze ist. Insbesondere, da dieser Mehraufwand hunderte Mal erfolgt.

  4. Stefan Macke sagt:

    Danke für dein Feedback. Dabei musste ich als erstes an dieses Zitat denken: Premature optimization is the root of all evil 🙂

    Aber du hast natürlich recht, was die Performance angeht. Die Frage ist aber, ob die Performance bei Programmiereinsteigern (schon) eine Rolle spielt. Dein letztes Beispiel würde ich meinen Azubis jedenfalls nicht empfehlen, weil sie meiner Erfahrung nach eher Probleme beim Verständnis des Codes hätten. Und mir geht es beim DRY-Prinzip in erster Linie um die Wartbarkeit des Codes. Da muss man leider häufig Abstriche bei anderen Kriterien – wie eben der Performance – machen. Bei Code für einen Mikrocontroller würde ich aber sicherlich deinen Weg bevorzugen! 😉

  5. Computerlinguist sagt:

    Das ist auch noch doppelt:
    boolean isFizz = i % 3 == 0;
    boolean isBuzz = i % 5 == 0;

    Das kann in eine Funktion ausgelagert werden:
    private boolean isDivisibleBy(int number, int divisor) {
        return number % divisor == 0;
    }
    boolean isFizz = isDivisibleBy(i, 3);
    boolean isBuzz = isDivisibleBy(i, 5);

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert

To create code blocks or other preformatted text, indent by four spaces:

    This will be displayed in a monospaced font. The first four 
    spaces will be stripped off, but all other whitespace
    will be preserved.
    
    Markdown is turned off in code blocks:
     [This is not a link](http://example.com)

To create not a block, but an inline code span, use backticks:

Here is some inline `code`.

For more help see http://daringfireball.net/projects/markdown/syntax