IT-Berufe-Podcast

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


Listen Later

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.

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.

          *

          Links
          • Permalink zu dieser Podcast-Episode
          • RSS-Feed des Podcasts
          • Don’t repeat yourself
          • DontRepeatYourself
          • OnceAndOnlyOnce
          • Shotgun surgery
          • Don´t Repeat Yourself (DRY)
          • ...more
            View all episodesView all episodes
            Download on the App Store

            IT-Berufe-PodcastBy Stefan Macke

            • 5
            • 5
            • 5
            • 5
            • 5

            5

            1 ratings


            More shows like IT-Berufe-Podcast

            View all
            Streitkräfte und Strategien by NDR Info

            Streitkräfte und Strategien

            46 Listeners

            Sternengeschichten by Florian Freistetter

            Sternengeschichten

            43 Listeners

            Geschichten aus der Geschichte by Richard Hemmer und Daniel Meßner

            Geschichten aus der Geschichte

            189 Listeners

            c’t uplink - der IT-Podcast aus Nerdistan by c’t Magazin

            c’t uplink - der IT-Podcast aus Nerdistan

            10 Listeners

            MORD AUF EX by Leonie Bartsch & Linn Schütze

            MORD AUF EX

            130 Listeners

            {ungeskriptet} - Gespräche, die dich weiter bringen by Ben Berndt

            {ungeskriptet} - Gespräche, die dich weiter bringen

            32 Listeners