Der Builder, der zählen konnte

In letzter Zeit habe ich mit Kotlin herumexperimentiert und bin begeistert, welche DSLs damit möglich sind. Das typsichere Builder-Pattern hatte ich hier schon öfter diskutiert, heute soll es zählen lernen. Eine Polygon-Klasse ist ein guter Kandidat für eine Beispielanwendung, denn so richtig „polygon“ ist es erst, wenn es mindestens drei Punkte hat. Starten wir also mit einer entsprechenden Klasse:

import javafx.geometry.Point2D
import javafx.scene.paint.Color

data class Polygon(val color: Color, val points: List)

Wir werden uns jetzt einen Builder schreiben, der dieses DSL erlaubt:

val polygon = builder()
            .withFirstPoint(3.0, 4.0)
            .withColor(Color.AQUA)
            .withSecondPoint(4.0, 7.0)
            .withThirdPoint(1.0, 3.0)
            .withPoint(6.0, 7.5)
            .withPoint(9.0, 2.0)
            .build()

Die Farbe kann an beliebiger Stelle gesetzt werden, und ist erforderlich. Der Aufruf von build() ist auch erst dann möglich, wenn mindestens drei Punkte gesetzt sind. Um die unterschiedliche Benennung der with???Point-Methoden kommen wir leider nicht herum, denn gegen Type-Erasure ist auch Kotlin machtlos.

Für die Farb-Variable benötigen wir ein Interface, wie wir es bereits von den vorigen Beispielen kennen:

interface Value<T>

data class With<T>(val value: T) : Value<T>

class Without<T> : Value<T>

Das Zählen der Punkte übernimmt ein weiteres Interface:

interface Counted

interface None : Counted

interface One : Counted

interface Two : Counted

interface ThreeOrMore : Counted

Nun können wir die Builder-Klasse schreiben:

class Builder<out V : Value<Color>, C : Counted>(val color: V, val points: List<Point2D>) {
    fun withColor(c: Color) = Builder<With<Color>, C>(With(c), points)
}

Moment mal, das ist ein bisschen wenig, oder? Stimmt, die geheime Soße fehlt noch, und zwar in Form von Extension-Methoden:

fun builder() = Builder<Without<Color>, None>(Without(), listOf())

fun <V : Value<Color>> Builder<V, None>.withFirstPoint(x: Double, y: Double) =
        Builder<V, One>(this.color, this.points + Point2D(x, y))

fun <V : Value<Color>> Builder<V, One>.withSecondPoint(x: Double, y: Double) =
        Builder<V, Two>(this.color, this.points + Point2D(x, y))

fun <V : Value<Color>> Builder<V, Two>.withThirdPoint(x: Double, y: Double) =
        Builder<V, ThreeOrMore>(this.color, this.points + Point2D(x, y))

fun <V : Value<Color>> Builder<V, ThreeOrMore>.withPoint(x: Double, y: Double) =
        Builder<V, ThreeOrMore>(this.color, this.points + Point2D(x, y))

fun Builder<With<Color>, ThreeOrMore>.build() = 
        Polygon(this.color.value, this.points)

Man kann sehr schön sehen, wie hier „mitgezählt“ wird. Natürlich wäre es noch schöner, wenn man alle Methoden withPoint nennen könnte, aber wie gesagt macht uns hier Type Erasure einen Strich durch die Rechnung.

Einen Schönheitsfehler hat das Ganze noch: Entgegen der Dokumentation war es mir nicht möglich, den Konstruktor von Builder private zu machen, die Extension-Methoden bekamen keinen Zugriff darauf, obwohl sie in der gleichen Datei standen.

Type-Level-Programming-Techniken wie die hier gezeigten sind nicht nur für Builder, sondern für alle möglichen DSLs interessant. Counted ist ein sogenannter Phantom-Typ, der niemals reale Werte hat, sondern nur dazu dient, zusätzliche „Garantien“ für einen Typ zu codieren. So könnte man SQL-Abfragen, die von einem Client kommen, mit einem Phantom-Typ versehen, der anzeigt, ob man sie schon auf SQL-Injection-Versuche getestet hat oder nicht – und dann kann man diesen Test nicht mehr vergessen, egal wo solche Werte im System herumschwirren.

So, das war jetzt nach – ahäm – längerer Pause endlich wieder mal ein Beitrag, und es hat Spaß gemacht, ihn zu schreiben. Ich denke, ich sollte wieder ein bisschen regelmäßiger bloggen…

Advertisements

Eine Builder-Variante mit Initialisierungsblöcken

Eines der weniger oft genutzen Java-Features sind Initialisierungsblöcke. Aber wenn sie dann einmal gebraucht werden, können sie ziemlich nützlich sein. Vielleicht auch, um Builder zu schachteln? Und wie könnte das aussehen?

Nehmen wir an, wir müssten eine einfache hierarchische Struktur aufbauen: Eine Menü-Leiste, darin Menüs, und in diesen wiederum Menüpunkte. Mit Initialisierungsblöcken könnte der Aufruf dann so aussehen:

MenuBarBuilder menuBarBuilder = new MenuBarBuilder() {{
    new Menu("menu1") {{
        new Item("item1.1");
        new Item("item1.2");
    }};
    new Menu("menu2") {{
        new Item("item2.1");
        new Item("item2.2");
        new Item("item2.3");
    }};
}};

System.out.println(menuBarBuilder);

Der Einfachheit halber bauen wir hier das Menü nicht zusammen (was ja nicht schwer ist, wenn man die Builder-Struktur erst einmal hat), sondern geben einfach nur eine String-Repräsentation aus. Hier wäre das Ergebnis:

menubar[
  menu 'menu1'[item 'item1.1', item 'item1.2'], 
  menu 'menu2'[item 'item2.1', item 'item2.2', item 'item2.3']]

Okay, bis auf die geschweiften Doppel-Klammern sieht die Verwendungsseite eigentlich gar nicht so schlimm aus. Aber welche Scheußlichkeiten müssen wir bei der Implementierung begehen, damit das funktioniert? Ich finde, auch die Implementierung ist recht erträglich, denn wir benutzen einen einfachen Trick: Menu ist eine innere Klasse von MenuBarBuilder, und kann sich somit bei der Objekterzeugung bei „seiner“ äußeren Instanz „registrieren“ (in diesem Fall einfach in eine vorgegebene Liste eintragen). Genauso ist Item eine innere Klasse von Menu und registriert sich dort. Dieser Aufbau löst eine Menge Probleme – und es wird nicht einmal ein statischer Import benötigt. Hier ist der Code:

import java.util.ArrayList;
import java.util.List;

public class MenuBarBuilder {

    private List<Menu> menus = new ArrayList<>();

    public String toString() {
        return "menubar" + menus.toString();
    }

    public class Menu {
        private final String menuName;
        private List<Item> items = new ArrayList<>();

        public Menu(String name) {
            this.menuName = name;
            MenuBarBuilder.this.menus.add(this);
        }

        public String toString() {
            return "\n  menu '" + menuName + "'" + items;
        }

        public class Item {
            private final String itemName;

            public Item(String name) {
                this.itemName = name;
                Menu.this.items.add(this);
            }

            public String toString() {
                return "item '" + itemName + "'";
            }

        }
    }

}

Ja, das ist alles was man braucht, damit der Aufruf oben funktioniert. Im Endeffekt ergibt sich die Einfachheit daraus, dass wir die Builder genau so ineinanderschachteln, wie auch unsere Struktur später aussehen soll.

Wie ist diese Builder-Variante nun einzuschätzen? Der größte Nachteil ist wohl, dass sie durch die Verwendung von Initialisierungsblöcken einfach ungewohnt ist. Weiterhin skaliert dieser Ansatz nicht so gut: Bei tiefen Hierarchien wird die Builder-Klasse immer länger, weil immer neue innere Klassen dazukommen, die man nicht auslagern kann. Außerdem wird für jeden verwendeten Initialisierungsblock eine anonyme Klassen erzeugt und eine entsprechende class-Datei angelegt, was bei viel geschachteltem „Inhalt“ ebenfalls problematisch sein kann (allerdings würde in diesem Fall auch das „normale“ Builder-Pattern unbequem werden). Auf der Haben-Seite dieses Konstrukts steht ein recht einfacher Aufbau und eine hohe Flexibilität.

Ehrlich gesagt bin ich mir nicht sicher, ob die vorgestellte Builder-Variante eine gute Idee ist, aber es war auf jeden Fall spannend, damit herumzuspielen, und überraschend, wie gut sie funktioniert.