Eine einfache Baumstruktur mit einer internen DSL erzeugen

Einführung

Wenn man einen komplexen Baum aus Objekten erstellen muss ist dieser setUp-Code in vielen Fällen schlecht lesbar. Es folgt ein Beispiel, in dem wir Objekte bzw. Knoten für Link Einstellungen im HTML erzugen müssen. Wenn ich nun im folgenden von einer DSL spreche meine ich eine DSL-API.
Listing 1
Node link = createNode(Node.class, "Link");
Node hover = createNode(Node.class, "hover");
Node hoverBackground = cretaeNode(Background.class, "background");
Node hoverFont = createNode(Font.class, "font");

Node normal = createNode(Node.class, "normal");
Node normalBackground = cretaeNode(Background.class, "background");
Node normalFont = createNode(Font.class, "font");

hover.add(hoverBackground);
hover.add(hoverFont);

normal.add(normalBackground);
normal.add(hoverFont);

link.add(hover);
link.add(normal);
Muss man die einzelnen Objekte nun noch konfigurieren wird das noch ein wenig komplexer:
Listing 2
Node link = createNode(Node.class, "Link");
Node hover = createNode(Node.class, "hover");
Node hoverBackground = cretaeNode(Background.class, "background");
Node hoverFont = createNode(Font.class, "font");
hoverFont.setColor("#00ff00");

Node normal = createNode(Node.class, "normal");
Node normalBackground = cretaeNode(Background.class, "background");
Node normalFont = createNode(Font.class, "font");
normalFont.setColor("#ff0000");
normalFoont.setFontDecoration("underline");

hover.add(hoverBackground);
hover.add(hoverFont);

normal.add(normalBackground);
normal.add(hoverFont);

link.add(hover);
link.add(normal);
Dem obigen Code mangelt es an Ausdrucksstärke. Es ist schwirieg zu erkennen, welcher Knoten wie konfiguriert wird und wie die Knoten untereinander verbunden sind. Angenehm wäre hier ein DSL ähnliches API welches die Konfiguration in den Vordergrund stellt.
Das erste Pattern welches wir hier zur Vereinfachung einführen können ist "Method Chaining". Es wäre dann möglich, das ganze kompakter zu gestalten und einige der lokalen Variablen zu entfernen.
Listing 3
Node link = createNode(Node.class, "Link")
              .add(createNode(Node.class, "hover")
                .add(cretaeNode(Background.class, "background"))
                .add(createNode(Font.class, "font")
                  .setColor("#00ff00"))

              .add(createNode(Node.class, "normal")
                .add(cretaeNode(Background.class, "background"))
                .add(createNode(Font.class, "font")
                  .setColor("#ff0000")
                  .setFontDecoration("underline"));
Ein Hauch einer DSL. Aber nur ein Hauch, den es bleibt immer noch das Problem, dass die Konstruktion und das Hinzufügen die Konfiguration überdeckt. Die Konstruktion ist für sich zudem unleserlich: Der Typ des Knoten wird als erstes Parameter übergeben und als zweiter Parameter ein String anhand dessen der Leser erkennen kann, was dieser Knoten darstellt. Mit diesem API werden die drei Komponenten Konfiguration, Konstruktion und Hinzufügen unterschiedlich stark in den Vordergrund gestellt wobei die Symmetrie der drei untereinander nicht gleich ist. Ideal wäre eine DSL, welche die Konfiguration, die Konstruktion und anschliessend das Hinzufügen in den Vordergrund stellt. Dazu verwenden wir als nächstes das "Nested Functions" Pattern an.
Listing 4
Node link = createLink(
              createHover(
                createBackground(),
                createFont()
                  .setColor("#00ff00")),

              createNormal(
                createBackground(),
                createFont()
                  .setColor("#ff0000")
                  .setFontDecoration("underline")));
Das untere ist klar dem oberen überlegen und mit nur zwei einfachen Pattern zu erreichen. Hier die technischen Details:

Jede Funktion muss anstelle von void ein Objekt zurück geben:
Listing 5
FontNode createFont() {
  FontNode node = createNode(Font.class, "font");
  return node;
}
Funktionen die Kinder haben können nehmen diese als Parameter entgegen:
Listing 6
Node createLink(Node...nodes) {
  Node link = createNode(Node.class, "link");
  for(Node nod: nodes) link.add(node);
  return link;
}


Node createHover(BackgroundNode bg, FontNode font) {
    Node hover = createNode(Node.class, "hover");
    hover.add(bg);
    hover.add(font);
    return hover;
}
Die erste Funktion createLink nimmt ein VarArg entgegen, da es nicht vorgegeben ist, ob ein Link immer "normal", "hover" etc hat. (In diesem Fall wären 0-N möglich). Ein "hover" oder "normal" Knoten braucht aber immer zwei Sub-Knoten: Background und Font. Daher verlangt die Funktion createHover immer beide Knoten.

Schlusswort

Wie man gesehen hat, braucht es für ein DSL-API nicht immer riesigen Aufwand. Es muss nicht immer ein Fluent-Interface oder sonstige komplexe Strukturen sein, einfache Sprachmittel wir VarArg und Method-Chaining reichen bereits aus. Weitere Informationen gibt es bei http://martinfowler.com/dslwip/InternalOverview.html