Quantcast
Channel: Stefan Macke, Autor bei IT-Berufe-Podcast
Viewing all articles
Browse latest Browse all 452

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

$
0
0

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.

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

Links


Viewing all articles
Browse latest Browse all 452