Dienstag, September 19, 2006

Refactoring und Closures

Refactoring-schwaches Java
Vor wenigen Tagen schrieb ich zusammen mit Henning Wolf Beispielcode für einen Sudoku-Solver. Wir wollten damit bestimmte Aspekte testgetriebener Entwicklung (TDD) sowie von Refactoring zeigen. Dabei haben wir unter anderem folgenden Code geschrieben:


public class Sudoku {

final static int groesse = 9;
private Zelle[][] _sudokuArray = new Zelle[groesse][groesse];

public Sudoku(int[][] array) {
for (int i = 0; i < array.length; i++) {
for (int j = 0; j < array[i].length; j++) {
_sudokuArray[i][j] = new Zelle(array[i][j]);
}
}
}

public String[][] gibAusgefuellt() {
String[][] result = new String[groesse][groesse];
for (int i = 0; i < result.length; i++) {
for (int j = 0; j < result[i].length; j++) {
result[i][j] = _sudokuArray[i][j].toString();
}
}
return result;
}

...
}



Hier hat man offensichtlich das DRY-Prinzip verletzt (DRY = Don't Repeat Yourself): Die zwei ineinander geschachtelten FOR-Schleifen existieren zweimal (wenn man sich den kompletten Quellcode des Sudoku-Solvers ansieht, kommt das Konstrukt sogar noch häufiger vor). Ähnliche Duplizierungen in Zusammenhang mit If-Abfragen, For- und While-Schleifen finden sich in jedem größeren Java-Programm, dass ich bisher gesehen habe.

Prinzipiell kann man diese Code-Duplizierungen auf verschiedene Arten beseitigen. Die naheliegendste Lösung besteht im Einführen eines Interfaces MatrixBesucher:


interfaces MatrixBesucher {
void besucheZelle (Object[][] matrix, int i, int j)
}



Ein Objekt dieses Interfaces reicht man als Parameter in eine Hilfemethode iteriereMatrix:


private static void iteriereMatrix (ZellenBesucher besucher) {
for (int i = 0; i < groesse; i++) {
for (int j = 0; j < groesse; j++) {
besucher.besucheZelle(i, j);
}
}
}



Dann wird aus dem Anfangs gezeigten Code:


public class Sudoku {

final static int groesse = 9;
private Zelle[][] _sudokuArray = new Zelle[groesse][groesse];

public Sudoku(final int[][] array) {
iteriereMatrix(new ZellenBesucher() {
public void besucheZelle (int i, int j) {
_sudokuArray[i][j] = new Zelle(array[i][j]);
}
}
}

public String[][] gibAusgefuellt() {
String[][] result = new String[groesse][groesse];
iteriereMatrix(new ZellenBesucher() {
public void besucheZelle (int i, int j) {
result[i][j] = _sudokuArray[i][j].toString();
}
}
return result;
}

...
}



Damit ist DRY wieder hergestellt. Das Problem daran ist nur, dass sowas kein Mensch macht. Schließlich ist die Lösung länger (mit Hilfsmethode und Interface 26 statt 16 Codezeilen) und besser lesbar ist sie leider auch nicht wirklich.

Möglicherweise schwerer wiegt, dass man für das Refactoring in Richtung DRY die Ebene wechseln musste. Das DRY-Prinzip war innerhalb einer Klasse auf Algorithmus-Ebene verletzt und wir mussten ein neues Interface einführen, also auf die Entwurfsebene wechseln. Das bedeutet, dass der Refactoringprozess an dieser Stelle nicht-linear verlaufen ist (linear: kleines Problem, kleine Lösung; nicht-linear: kleines Problem, umständliche Lösung).

Anmerkung: Man kann sich andere Lösungen des Problems vorstellen, die ohne die umständlichen und schwer lesbaren anonymen Inner-Classes auskommen. Solche Lösungen erfordern aber deutlich mehr Entwurfsarbeit, mitunter sogar Vererbung oder den Einsatz von Entwurfsmustern. Damit würde der Refactoring-Prozess sogar noch nicht-linearer.

Refactoring mit Closures
Mit Closures hingegen ließe sich das DRY-Prinzip mit wenig Aufwand umsetzen und man kann beim Refactoring auf der Algorithmus-Ebene bleiben. Zu allem Überfluss ist die Lösung auch noch leicht lesbar. Hätte Java Closures, könnte die Lösung in etwa so aussehen (die Syntax für Closures ist hier an Groovy angelehnt):


public class Sudoku {

final static int groesse = 9;
private Zelle[][] _sudokuArray = new Zelle[groesse][groesse];

public Sudoku(final int[][] array) {
iteriereMatrix({i, j -> _sudokuArray[i][j] = new Zelle(array[i][j])};
}

public String[][] gibAusgefuellt() {
String[][] result = new String[groesse][groesse];
iteriereMatrix({i, j -> result[i][j] = _sudokuArray[i][j].toString()};
return result;
}

private static void iteriereMatrix (Closure closure) {
for (int i = 0; i < groesse; i++) {
for (int j = 0; j < groesse; j++) {
closure.execute(i, j);
}
}
}

...
}



Und schon ist die Lösung nur noch 15 Codezeilen lang (im Gegensatz zu 16 Codezeilen beim Original-Quelltext).

Nun kann man natürlich einwenden, dass die Closures prinzipiell auch nicht mächtiger sind als anonyme Inner-Classes, es sich hier also lediglich um Syntactic-Sugar handelt. Das ist richtig und zeigt, wie wichtig eine prägnante Syntax ist. Denn die Inner-Class-Lösung wird in der Praxis so gut wie nicht eingesetzt, während die Closure-Lösung überaus üblich ist in Programmiersprachen, die Closures unterstützen wie z.B. Lisp, Python der Groovy.

Closures in Java
Wenn Closures so nützlich sind und konzeptionell eigentlich nur Syntactic-Sugar für anonyme Inner-Classes darstellen, dann sollte es ja eigentlich auch kein Problem sein, Closures auch in Java zu unterstützen. Und tatsächlich gibt es Anzeichen dafür, dass wir irgendwann Closures auch in Java haben werden:

http://article.gmane.org/gmane.comp.lang.lightweight/2274

Interessant ist hier die Argumentation: Es ist ein grundlegendes Design-Prinzip von Java, dass nur dann Objekte auf dem Heap allokiert werden, wenn der Programmierer dies explizit anfordert (typischerweise durch new). Bei Closures muss man aber implizit Speicher auf dem Heap allokieren und daher passen Closures nicht zum Design von Java.
Nun bricht aber seit Java 5 das Auto-Boxing dieses Prinzip und daher sei es jetzt sowieso egal und man könnte dann auch noch Closures einführen :-)

Prinzipiell bin ich persönlich der Meinung, dass Closures mir das Leben als Java-Entwickler erleichtern würden. Gleichzeitig müsste man aber große Teile des JDKs komplett umbauen. Sonst startet der ganze Ansatz als Tiger und landet als Bettvorleger. Wenn man schon Closures hat, wird man auch erwarten, dass mind. die Collections massiv davon Gebrauch machen (z.B. für das Iterieren über die Elemente). Wo man sich sonst noch die Verwendung von Closures wünschen würde, kann man gut am GDK (Groovy Development Kit) sehen.

Und wenn man soweit geht, dann sollte man vielleicht lieber Java als Sprache auf Deprecated setzen und die Migration nach Groovy pushen. Denn da werden Closures von Beginn an unterstützt und auch entsprechende Libraries im GDK sind verfügbar.

Post bewerten

Keine Kommentare: