Aufwertung von Rückgabetypen

#1

Ich habe eine Frage zur “Aufwertung” von Rückgabetypen geerbter Methoden.

Ich habe eine Klasse XmlBuilder, mit der man XML-Dokumente erzeugen kann, bei denen sich der Builder die richtige Einrückebene merkt. Ein Beispiel:

    String xml = new XmlBuilder()
        .appendXmlHeader()
        .appendOpeningTag("zeug")
        .appendInTag("foo", "Wichtiger foo-Inhalt")
        .appendInTag("bar", "Wichtiger bar-Inhalt")

        .appendOpeningTag("baz")
        .appendInTag("baz-foo", "baz-foo-blubb",
                new NamedXmlParameter("foo", "foo-Inhalt"))
        .appendInTag("baz-bar", "baz-bar-blubb",
            new NamedXmlParameter("foo", "foo-Inhalt"),
            new NamedXmlParameter("bar", "bar-Inhalt"),
            new NamedXmlParameter("baz", "baz-Inhalt"))
        .appendClosingTag("baz")

        .appendOpeningTagWithParameters("open-mit-parameter",
                new NamedXmlParameter("option", "bla bla"))
        .appendClosingTag("open-mit-parameter")
        .appendClosingTag("zeug")
        .toString();

Erzeugt

<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>

<zeug>
    <foo>Wichtiger foo-Inhalt</foo>
    <bar>Wichtiger bar-Inhalt</bar>
    <baz>
        <baz-foo foo="foo-Inhalt">baz-foo-blubb</baz-foo>
        <baz-bar foo="foo-Inhalt" bar="bar-Inhalt" baz="baz-Inhalt">baz-bar-blubb</baz-bar>
    </baz>
    <open-mit-parameter option="bla bla">
    </open-mit-parameter>
</zeug>

Natürlich lässt sich das auch wie folgt erreichen:

    XmlBuilder builder = new XmlBuilder();
    builder.appendXmlHeader();
    builder.appendOpeningTag("zeug");
    builder.appendInTag("foo", "Wichtiger foo-Inhalt");
    builder.appendInTag("bar", "Wichtiger bar-Inhalt");

    builder.appendOpeningTag("baz");
    builder.appendInTag("baz-foo", "baz-foo-blubb",
            new NamedXmlParameter("foo", "foo-Inhalt"));
    builder.appendInTag("baz-bar", "baz-bar-blubb",
            new NamedXmlParameter("foo", "foo-Inhalt"),
            new NamedXmlParameter("bar", "bar-Inhalt"),
            new NamedXmlParameter("baz", "baz-Inhalt"));
    builder.appendClosingTag("baz");

    builder.appendOpeningTagWithParameters("open-mit-parameter",
            new NamedXmlParameter("option", "bla bla"));
    builder.appendClosingTag("open-mit-parameter");
    builder.appendClosingTag("zeug");

    String xml = builder.toString();

Man sieht schon, die erste Version ist deutlich netter zu schreiben und übersichtlicher, weil nicht die ganzen Wiederholungen des Builders und die Semikoli den Blick auf das Wesentliche erschweren. Dafür gibt jede Methode im XmlBuilder this zurück.

So weit, so gut. Nun gibt es auch einen HtmlBuilder, der auf analoge Weise funktioniert. Und da HTML nicht viel mehr als ein Spezialfall von Xml ist, erbt der HtmlBuilder vom XmlBuilder und verwendet viele von den Methoden des XmlBuilders wieder, was gut ist, da es dem DRY-Principle (don’t repeat yourself) entspricht. Leider enthält der HtmlBuilder nun viele Methoden der Form

@Override
public HtmlBuilder appendOpeningTag(String tag) {
    super.appendOpeningTag(tag);
    return this;
}

damit der Aufruf analog zum XmlBuilder wie folgt erfolgen kann:

    String table = new HtmlBuilder()
        .appendOpeningTable()
        .appendOpeningThead()
        .appendOpeningTr()
        .appendTh("Zeug")
        .appendLeftAlignedTh("Anderes Zeug")
        .appendRightAlignedTh("Noch mehr Zeug")
        .appendLeftAlignedThWithClass("Zeug mit Klasse", "CSS-KLASSE")
        .appendClosingTr()
        .appendClosingThead()
        .appendOpeningTbody()
        .appendOpeningTr()
        .appendTd("foo")
        .appendTd("bar")
        .appendRightAlignedTd("baz")
        .appendTd("blubb", "CSS-KLASSE")
        .appendClosingTr()
        .appendOpeningTr()
        .appendTd("foo 2")
        .appendTd("bar 2")
        .appendTd("baz 2")
        .appendTd("blubb 2")
        .appendClosingTr()
        .appendClosingTbody()
        .appendOpeningTfoot()
        .appendP("Blubber")
        .appendClosingTfoot()
        .appendClosingTable()
        .toString();

Dabei kommt dann sowas raus:

<table>
    <thead>
        <tr>
            <th>Zeug</th>
            <th align="left">Anderes Zeug</th>
            <th align="right">Noch mehr Zeug</th>
            <th class="CSS-KLASSE" align="left">Zeug mit Klasse</th>
        </tr>
    </thead>
    <tbody>
        <tr>
            <td>foo</td>
            <td>bar</td>
            <td align="right">baz</td>
            <td class="CSS-KLASSE">blubb</td>
        </tr>
        <tr>
            <td>foo 2</td>
            <td>bar 2</td>
            <td>baz 2</td>
            <td>blubb 2</td>
        </tr>
    </tbody>
    <tfoot>
        <p>Blubber</p>
    </tfoot>
</table>

Ohne die oben gezeigten Weiterleitungsmethoden wäre in der Kette bereits nach appendOpeningTable() Schluss, welches intern die Methode appendOpeningTag() aus dem XmlBuilder aufrufen würde. Und der Rückgabewert, nämlich der XmlBuilder, kennt dann natürlich nicht die Methode appendOpeningThead(), wodurch zurecht ein Kompilerfehler entsteht.

Das ist ziemlich schade, da so recht viele dieser Weiterleitungsmethoden im HtmlBuilder stehen, aber noch fataler ist es in einem fachlich erweiterten HtmlBuilder, der speziell auf das Projekt zugeschnittene Javascripte und dergleichen erzeugt und bestimmte Methoden anbietet, die außerhalb davon wenig Sinn machen. Hier habe ich darauf verzichtet, die vielen, vielen Methoden des HtmlBuilders weiterzuleiten, wodurch nur die umständlichere Verwendung mit der Wiederholung des builders in jeder Zeile verbleibt.

Gibt es da irgendeinen Trick, wie ich nicht ein Objekt der Klasse, sondern ein ggf. abgeleitetes Objekt, nämlich das der Klasse, die eigentlich aktiv ist, zurückgebe? Sozusagen ein “supper.this” oder sowas. Vielleicht mit extends oder so? Wenn ich nach dem Themenkomplex google, lande ich bei Seiten, die mir Vererbung erklären, aber nicht auf dieses Problem eingehen.

#2

“Self Types” dürften da das passende sein: https://www.sitepoint.com/self-types-with-javas-generics/

u.U. muss man dann nur noch eine abstrakte Klasse mit allen geteilten Methoden einführen, damit sauber wird.

1 Like
#3

Das ist schon mal sehr gut, da wird genau das gleiche Problem beschrieben, ich bin auf die Lösung gespannt! Danke für den Link.

#4

Wie wäre es mit favor composition over inharitance?
Erwartet der aufrufende Code wirklich, dass HtmlBuilder von XmlBuilder erbt oder dient das ausschließlich der “Wiederverwendung” von XmlBuilder?

bye
TT

#5

Will man Komposition verwenden so muss man aber auch all die Methoden weiterreichen und hat nichts an Aufwand gewonnen. Gerade wenn man einen etwas erweiterten HtmlBuilder in einem Projekt verwendet, hätte man dann all die HtmlBuilder-Methoden weiterzureichen.

Und inhaltlich ist an dieser Stelle imho Vererbung ausnahmsweise das geeignete Werkzeug.

Die oben gezeigte Lösung ist mir zu viel Gewiggel und bringt zu viel schwer verständliches in eigentlich recht schlichte Zusammenhänge. Eigentlich fehlt hier ja nur ein Zugriff, den die Sprachdefinition nachreichen müsste.

#6

Das sehe ich nicht als KO-Kriterium. Eclipse (wie wohl auch jede andere IDE) kann dir ja diese delegation methods generieren.

Es wäre zumindest die sauberste und einfachste Lösung für Dein Problem.

bye
TT

#7

Ich denke allgemein auch, dass man um irgendeine Form des Weiterreichens der Methoden kaum herumkommt.

Die genaue Ausprägung davon ist aber nochmal interessant. Man könnte natürlich sowas machen:

class XmlBuilder {
    XmlBuilder addXmlTag() { 
        add("foo");
        return this;
    }
}

class HtmlBuilder extends XmlBuilder {
    HtmlBuilder addHtmlTag() { 
        add("bar");
        return this;
    }

    @Override
    HtmlBuilder addXmlTag() { 
        super.addXmlTag();
        return this;
    }
}

Die Frage, wie das tatsächlich implementiert ist, ist etwas subtil. Natürlich wird da letztendlich wohl ein Haufen Zeug in einen StringBuilder gesteckt. Aber eine wichtige Frage ist: Muss die speziellere Klasse (d.h. HtmlBuilder) etwas über den Zustand ihrer Elternklasse wissen, was nicht public ist? Vermutlich schon - z.B. könnte es nötig sein, für addHtmlTag auf die aktuelle Einrücktiefe zuzugreifen, die dann (ggf. mit einem protected getter) vom XmlBuilder geholt werden muss (nicht zwingend - nur ein Beispiel).

Die Frage hat nämlich Einfluß darauf, ob man “composition over inheritance” überhaupt verwenden kann.

Eine etwas elementarere Frage ist: Was gewinnt man mit “composition over inheritance”, bzw. was genau modelliert man damit? Ganz platt gesagt: Ohne ein interface macht das vielleicht gar keinen Sinn.

Konkreter: Wie würde man “composition over inheritance” hier umsetzen? :

class HtmlBuilder {  // extends XmlBuilder oder nicht?

   private final XmlBuilder xmlBuilder;

    HtmlBuilder addHtmlTag() { 
        add("bar");
        return this;
    }

    HtmlBuilder addXmlTag() { 
        xmlBuilder.addXmlTag();
        return this;
    }
}

D.h. wo liegt z.B. der StringBuilder, wo das ganze reinkommt? Und muss XmlBuilder dann vielleicht eine Methode haben wie getCurrentIndentation()?


Einen Schritt zurück: Ich bin ja ein interface-Fan, und sehe public class immer mit einer gewissen Skepsis. Und ich denke, dass man sich damit hier auch einige Freiheiten schaffen kann, was die Implementierung angeht. Also, nur aus dem Bauch heraus würde ich erstmal

interface XmlBuilder {
    XmlBuilder addXmlTag();
}

interface HtmlBuilder extends XmlBuilder {
    HtmlBuilder addHtmlTag();

    @Override
    HtmlBuilder addXmlTag() ;
}

defnieren und dann weitersehen. Dass man dann nicht mehr

String xml = new XmlBuilder().addXmlTag().toString();

schreiben würde, sondern

String xml = XmlBuilders.create().addXmlTag().toString();

ändert an der Nutzbarkeit ja nichts.

#8

Nein, der XmlBuilder kümmert sich derart darum, dass abgeleitete Klassen gar nichts mit dieser Verwaltung zu tun haben, da sie zum Hinzufügen die Methoden des XmlBuilders nutzen.

Schade dass da in Java das Sprachmittel fehlt, um es elegant zu lösen. Da werde ich es dann so belassen wie es ist.

Klar könnte ich mir bei Komposition momentan die ganzen Methoden von Eclipse generieren lassen, das ganze bleibt aber ein Wartungsaufwand, weil ich für jede weiter unten hinzugefügte Methode dann weiter oben daran denken muss, sie zu delegieren.

Imho ist das hier ein echt wirklich sinniger Fall für Vererbung, auch wenn ich sonst Komposition bevorzuge. Aber das mag natürlich jeder anders sehen.

#9

Der Trick ist, dass du noch eine Klasse dazwischen brauchst, du erstellst dir eine KlasseAbstractXMLBuilder und darein packst du alle deine Methoden, die du jetzt im XMLBuilder hast. Diese abstrakte Klasse hat einen generischen Parameter T extends AbstrastractXMLBuilder<T> und eine abstrakte Methode T getThis() anstelle von return this; machst du überall return getThis();. Das sieht dann ungefähr so aus:

public abstract class AbstractXMLBuilder<T extends AbstractXMLBuilder<T>> {
  
  protected final StringBuilder content = new StringBuilder();
    
  protected abstract T getThis();
  
  public T appendStartTag(String name) {
    content.append("<" + name + ">");
    return getThis();
  }
  
  public T appendEndTag(String name) {
    content.append("</" + name + ">");
    return getThis();
  }
  
  public String toString() {
    return content.toString();
  }
  
}

Davon abgeleitet kannst du dann eine Klasse XMLBuilder erstellen, die dann so aussieht:

public class XMLBuilder extends AbstractXMLBuilder<XMLBuilder> {

  @Override
  protected XMLBuilder getThis() {
    return this;
  }  
}

Und eine Klasse HTMLBuilder die so aussieht:

public class HTMLBuilder extends AbstractXMLBuilder<HTMLBuilder> {

  @Override
  protected HTMLBuilder getThis() {
    return this;
  }
  
  public HTMLBuilder addBr() {
    content.append("</br>");
    return getThis();
  }
  
}
1 Like
#10

Ha, der Link zu http://www.angelikalanger.com/GenericsFAQ/FAQSections/ProgrammingIdioms.html#FAQ206 lag mir schon auf der… … … Tastatur, aber irgendwie hat er es nicht geschafft. Ich finde, das ist in diesem Fall eine sinnvolle Lösung.

#11

Das sieht schon mal besser aus, aber es bleibt “eigentlich unnötiges Gewiggel”, wenn Java zuließe, this auf die eigentliche Klasse weiterzureichen.

#12

Mir sind ein paar potentielle Fehler in meiner Bibliothek aufgefallen - wegen verwendeten Raw Types - und das hat mich sofort an diesen Thread erinnert.

In einem SceneGraph hat jeder Knoten mehrere Kinder und ein Elternteil. Das Problem ist dann kurz gefasst das hier:

public class Node {
    public Node(Parent<THIS> parent) {
    }
}

Leider hat weder Java noch Kotlin Self Types. Es gibt aber eine Bibliothek namens “Manifold” die “@Self” als Compiler Parameter hinzufügt. Das sieht dann so aus:

public class Node {
    public Node(Parent<@Self Node> parent) {
    }
}

Dann muss jede verebende Klasse nicht Parent<Node> sondern Parent<ExtendedNode> übergeben.

Funktioniert leider Gottes nicht für Android! :roll_eyes:
Für Android hab ich dann eine Überprüfung zur Laufzeit in etwa so eingeführt. Nicht optimal, aber besser als gar nichts.

public class Node {
    public <N extends Node> void setParent(N node, Parent<N> p) {
        if( node != this )
            throw new IllegalArgumentException( "XY" );
    }
}