Samstag, September 23, 2006

Video: Closures in Java

Es gibt ein ganz lustiges und informatives Video, wie Closures in Java aussehen könnten und wie sie intern auf Inner Classes abgebildet werden können.

Post bewerten

Freitag, September 22, 2006

Terminplaner in Lisp

Ich habe das Terminplaner-Beispiel zuerst in Java programmiert und dann nach Groovy (Skriptsprache für die Java-Plattform) programmiert. Das Beispiel ist in Groovy als eine der jüngsten Programmiersprachen doch erheblich kürzer und einfacher als in Java. Da hat es mich dann doch gereizt, das Beispiel auch einmal in einer der ältesten Programmiersprachen zu programmieren: Lisp. Dabei habe ich Common Lisp verwendet mit CLOS (Common Lisp Object System) und Lisp-Unit von Chris Riesbeck.

Die quantitativen Ergebnisse aller drei Implementationen zuerst einmal im Vergleich:

Java

  • 5 Fachklassen, 1 Exception-Klasse, 1 Testklasse

  • 37 Methoden inkl. Konstruktoren + 10 Methoden in Testklasse

  • 246 LOC operativ + 144 LOC für Testklasse (inkl. Leerzeilen)

  • 4 For-Schleifen, 4 If-Abfragen


Groovy

  • 4 Fachklassen, 1 Exception-Klasse, 1 Testklasse

  • 30 Methoden inkl. Konstruktoren + 9 Methoden in Testklasse

  • 203 LOC operativ + 123 LOC für Testklasse (inkl. Leerzeilen)

  • Alle 4 For-Schleifen der Java-Lösung wurden durch Closures ersetzt, die 4 If-Abfragen blieben bestehen.


Common Lisp

  • 4 Fachklassen, 1 Testklasse - Exceptions werden mit Hilfe von Conditions modelliert (nur eine Zeile notwendig)

  • 17 Methoden, 1 Condition-Funktion, 10 Testmethoden

  • 76 LoC operativ, 100 LoC Test (inkl. Leerzeilen)

  • Alle 4 Schleifen der Java-Lösung wurden durch Closures ersetzt, die 4 If-Abfragen blieben bestehen, allerdings in den spezialisierten Varianten when und unless.


Wow! Die Common-Lisp-Variante hat erheblich weniger Code als die Java- oder die Groovy-Variante. Woran liegt das?
Naheliegend wäre es, die Ursache bei dem Lisp-Feature zu suchen, dass keine andere Programmiersprache anbietet: Macros (Vorsicht: Lisp-Makros sind ganz anders als die berüchtigten C-Makros). Dem ist aber nicht so. Tatsächlich habe ich in dem Lisp-Code kein eigenes Makro selbst definiert und nur an einer Stelle von der Mächtigkeit der
Lisp-Makros profitiert. Mit assert-error aus Lisp-Unit kann man sehr einfach prüfen, ob eine Exception (im Lisp-Jargon Condition) geworfen wird:


(define-test benutzer-nicht-eingeladen-exception
(setup)
(let ((termin (make-termin stefans-kalender ein-datum 180 "TDD-Dojo")))
(assert-error 'benutzer-nicht-eingeladen (lehne-termin-ab termin "Henning"))))


Zum Vergleich der entsprechende Java-Code:


public void testBenutzerNichtEingeladenException() {
Termin termin = _stefansKalender.newTermin(_jetzt, 180, "TDD-Dojo");
try {
termin.lehneTerminAb(HENNING);
fail("BenutzerNichtVorhandenException erwartet");
} catch (BenutzerNichtEingeladenException e) {
assertTrue("Exception erwartet", true);
}
}


Das erklärt aber nicht, warum der operative Code in Lisp so viel kürzer ist als in Java oder Groovy. Eine nähere Analyse fördert folgende Gründe zu Tage:
  • In Java und Groovy werden eigene Zeilen spendiert, für schließende geschweifte Klammern. Für das Lisp-Äquivalent (schließende runde Klammern) werden keine eigenen Zeilen spendiert. Dierk König schlägt in seinem kommenden Groovy-Buch diese Art der Formatierung für Groovy für bestimmte Situationen auch vor (bei den Buildern).

    • In Java und Groovy ist es üblich, Leerzeilen in Methoden einzufügen, teilweise auch zwischen Attributen. Beides macht man in Lisp eher nicht.

    • Das aufschreiben von Attributen einer Klasse ist in Common-Lisp schlanker als in Groovy oder Java. Insbesondere gibt es prägnante Abkürzungen, um Setter und Getter und Defaultwerte zu definieren.

    • Einige Implementierungen lassen sich nicht sinnvoll 1:1 nach Common-Lisp transferieren. Daher vergleicht man ein Stück weit dann doch Apfelsinen mit Orangen (so groß wie zwischen Äpfel und Birnen sind die Unterschiede dann doch nicht :-)


    Der gewaltige Unterschied in den LoC findet sich bei der Menge der Syntaxelemente in deutlich reduzierter Form. So spart man sich in Common-Lisp zwar Einiges an Zeilen für die Attributdefinitionen in Klassen, es gibt aber die gleiche Anzahl von Attributdeklarationen. In diesem Sinne ist die Lisp-Lösung zwar kürzer als die Lösungen in Java und Groovy. Sie ist bzgl. der Komplexität aber äquivalent zur Groovy-Lösung und diese beiden Lösungen sind weniger komplex als die Java-Lösung.

    Wieviel wirkt der große Unterschied in den LoC? Quelltext wird viel häufiger gelesen als geschrieben. Daher ist die Einsparung der Tastenanschläge bei der Code-Erstellung nicht wirklich von Interesse. Wenn man allerdings beim Lesen von Quelltext weder vertikal noch horizontal scrollen muss, erleichtert dies das Lesen: Im Lisp-Terminplaner passt jede operative Klasse problemlos auf eine Bildschirmseite, so dass man jede Klasse auf einen Blick erfassen kann. Das kann man als (leichten) Vorteil von Lisp werten. Allerdings muss ich zugeben, dass man den Groovy-Code an der einen oder anderen Stelle noch etwas eleganter Formulieren kann und dann auch noch etwas an LoC einsparen kann.

    Quelltext des Terminplaners in Common-Lisp.

    Post bewerten

  • 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

    Montag, September 18, 2006

    Groovy und Grails auf Konferenzen

    Eindrücke aus erster Hand zu Groovy und Grails kann man sich auf verschiedenen Konferenzen in den nächsten Monaten besorgen:

    Post bewerten

    Grails: Schnell mal eine Web-Anwendung mit Groovy für die Java-Plattform

    An anderer Stelle habe ich bereits Groovy kurz beleuchtet, die standardisierte Skriptsprache für die Java-Plattform.

    Jetzt, wo man eine so mächtige und flexible Skriptsprache hat, lag es natürlich nahe, den Rails-Ansatz auch für Groovy nutzbar zu machen. Herausgekommen ist Grails.

    Grails existiert erst in der Version 0.2.2, hat aber bereits einen erstaunlichen Entwicklungsstand. Mit wenigen Clicks ist eine erste Version der Webanwendung zusammengebaut, die man dann explorativ weiterentwickeln kann.

    Die existierende Dokumentation reicht für erste Schritte aus, vieles kann man sich durch nachdenken erschließen.

    Grails ist sicherlich eine sehr interessante Technologie für die Web-Projekte, die für die Java-Plattform entwickeln müssen/wollen. Die Java-ähnliche Syntax von Groovy erleichtert Java-Entwicklern den schnellen Einstieg.

    Post bewerten

    Sonntag, September 03, 2006

    Kommentarfunktion in diesem Blog jetzt für alle

    jedenfalls für alle mit einem Blogger-Account. Bisher musste man zusätzlich Geheimwissen mitbringen: Man musste auf das # am Ende eines Eintrages klicken, um den Eintrag einzeln zu öffnen und dort konnte man dann einen Kommentar hinterlassen. Ich habe jetzt das Template für diesen Blog so geändert, dann man die Kommentarfunktion jetzt direkt am Eintrag aufrufen kann. Außerdem habe ich das kryptische # durch "Permanent Link" ersetzt.

    Dass man zum Kommentar hinterlassen einen Blogger-Account haben muss, ist ärgerlich, aber wg. der Spam-Problematik zur leider wohl unvermeidlich.

    Post bewerten