Don’t Repeat Yourself (DRY) – Wissenshäppchen #1
IT-Berufe-Podcast - Ein Podcast von Stefan Macke - Montags
Kategorien:
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)
