it-swarm.com.de

Builder Pattern und Vererbung

Ich habe eine Objekthierarchie, deren Komplexität mit der Vertiefung des Vererbungsbaums zunimmt. Keines davon ist abstrakt, daher dienen alle ihre Instanzen einem mehr oder weniger ausgefeilten Zweck.

Da die Anzahl der Parameter sehr hoch ist, möchte ich das Builder-Pattern verwenden, um Eigenschaften festzulegen, anstatt mehrere Konstruktoren zu codieren. Da ich mich auf alle Permutationen einstellen muss, hätten Blattklassen in meinem Vererbungsbaum Teleskopkonstruktoren.

Ich habe hier nach einer Antwort gesucht, als ich beim Design auf Probleme stieß. Lassen Sie mich zunächst ein einfaches, flaches Beispiel geben, um das Problem zu veranschaulichen.

public class Rabbit
{
    public String sex;
    public String name;

    public Rabbit(Builder builder)
    {
        sex = builder.sex;
        name = builder.name;
    }

    public static class Builder
    {
        protected String sex;
        protected String name;

        public Builder() { }

        public Builder sex(String sex)
        {
            this.sex = sex;
            return this;
        }

        public Builder name(String name)
        {
            this.name = name;
            return this;
        }

        public Rabbit build()
        {
            return new Rabbit(this);
        }
    }
}

public class Lop extends Rabbit
{
    public float earLength;
    public String furColour;

    public Lop(LopBuilder builder)
    {
        super(builder);
        this.earLength = builder.earLength;
        this.furColour = builder.furColour;
    }

    public static class LopBuilder extends Rabbit.Builder
    {
        protected float earLength;
        protected String furColour;

        public LopBuilder() { }

        public Builder earLength(float length)
        {
            this.earLength = length;
            return this;
        }

        public Builder furColour(String colour)
        {
            this.furColour = colour;
            return this;
        }

        public Lop build()
        {
            return new Lop(this);
        }
    }
}

Nun, da wir etwas Code zum Weiterarbeiten haben, möchte ich eine Lop erstellen:

Lop lop = new Lop.LopBuilder().furColour("Gray").name("Rabbit").earLength(4.6f);

Dieser Aufruf wird nicht kompiliert, da der letzte verkettete Aufruf nicht aufgelöst werden kann. Builder definiert nicht die Methode earLength. Auf diese Weise müssen also alle Aufrufe in einer bestimmten Reihenfolge verkettet werden, was insbesondere mit einem tiefen Hierarchiebaum sehr unpraktisch ist.

Bei meiner Suche nach einer Antwort stieß ich auf Subclassing einer Java Builder-Klasse , die die Verwendung des Curiously Recursive Generic Pattern vorschlägt. Da meine Hierarchie keine abstrakte Klasse enthält, funktioniert diese Lösung jedoch nicht für mich. Aber der Ansatz basiert auf Abstraktion und Polymorphismus, um funktionieren zu können. Deshalb glaube ich nicht, dass ich ihn an meine Bedürfnisse anpassen kann.

Ein Ansatz, mit dem ich mich aktuell entschieden habe, ist, alle Methoden der Superklasse Builder in der Hierarchie zu überschreiben und einfach Folgendes zu tun:

public ConcreteBuilder someOverridenMethod(Object someParameter)
{
    super(someParameter);
    return this;
}

Mit diesem Ansatz kann ich sicher sein, dass mir eine Instanz zurückgegeben wird, für die Kettenaufrufe ausgegeben werden können. Dies ist zwar nicht so schlimm wie das Teleskop-Anti-Pattern, aber es ist eine Sekunde und ich halte es für etwas "hacky".

Gibt es eine andere Lösung für mein Problem, von der ich keine Kenntnis habe? Vorzugsweise eine Lösung, die mit dem Entwurfsmuster übereinstimmt. Vielen Dank!

45
Eric

Dies ist sicherlich mit der rekursiven Begrenzung möglich, aber die Builder für Subtypen müssen ebenfalls generisch sein, und Sie benötigen einige abstrakte Zwischenklassen. Es ist etwas umständlich, aber es ist immer noch einfacher als die nicht generische Version.

/**
 * Extend this for Mammal subtype builders.
 */
abstract class GenericMammalBuilder<B extends GenericMammalBuilder<B>> {
    String sex;
    String name;

    B sex(String sex) {
        this.sex = sex;
        return self();
    }

    B name(String name) {
        this.name = name;
        return self();
    }

    abstract Mammal build();

    @SuppressWarnings("unchecked")
    final B self() {
        return (B) this;
    }
}

/**
 * Use this to actually build new Mammal instances.
 */
final class MammalBuilder extends GenericMammalBuilder<MammalBuilder> {
    @Override
    Mammal build() {
        return new Mammal(this);
    }
}

/**
 * Extend this for Rabbit subtype builders, e.g. LopBuilder.
 */
abstract class GenericRabbitBuilder<B extends GenericRabbitBuilder<B>>
        extends GenericMammalBuilder<B> {
    Color furColor;

    B furColor(Color furColor) {
        this.furColor = furColor;
        return self();
    }

    @Override
    abstract Rabbit build();
}

/**
 * Use this to actually build new Rabbit instances.
 */
final class RabbitBuilder extends GenericRabbitBuilder<RabbitBuilder> {
    @Override
    Rabbit build() {
        return new Rabbit(this);
    }
}

Es gibt eine Möglichkeit, die "konkreten" Blattklassen zu vermeiden, wenn wir dies hätten:

class MammalBuilder<B extends MammalBuilder<B>> {
    ...
}
class RabbitBuilder<B extends RabbitBuilder<B>>
        extends MammalBuilder<B> {
    ...
}

Dann müssen Sie neue Instanzen mit einem Diamanten erstellen und im Referenztyp Platzhalter verwenden:

static RabbitBuilder<?> builder() {
    return new RabbitBuilder<>();
}

Das funktioniert, weil die Begrenzung der Typvariablen gewährleistet, dass alle Methoden von z. RabbitBuilder hat einen Rückgabetyp mit RabbitBuilder, auch wenn das Typargument nur ein Platzhalter ist.

Ich bin kein großer Fan davon, weil Sie überall Platzhalter verwenden müssen und Sie nur eine neue Instanz erstellen können, indem Sie den Diamanten oder einen raw-Typ verwenden. Ich nehme an, dass Sie so oder so mit einer kleinen Unbeholfenheit enden.


Und übrigens dazu:

@SuppressWarnings("unchecked")
final B self() {
    return (B) this;
}

Es gibt eine Möglichkeit, diesen ungeprüften Cast zu vermeiden, dh die Methode abstrakt zu machen:

abstract B self();

Und überschreiben Sie es dann in der Blatt-Unterklasse:

@Override
RabbitBuilder self() { return this; }

Das Problem dabei ist, dass die Unterklasse zwar etwas typsicherer ist, jedoch etwas anderes als this zurückgeben kann. Grundsätzlich hat die Unterklasse die Möglichkeit, etwas Falsches zu tun, daher sehe ich keinen wirklichen Grund, einen dieser Ansätze dem anderen vorzuziehen.

50
Radiodef

Wenn jemand noch auf das gleiche Problem stößt, schlage ich die folgende Lösung vor, die dem Entwurfsmuster "Zusammensetzung bevorzugen über Vererbung" entspricht.

Elternklasse

Das Hauptelement davon ist die Schnittstelle, die der Builder für übergeordnete Klasse implementieren muss:

public interface RabbitBuilder<T> {
    public T sex(String sex);
    public T name(String name);
}

Hier ist die geänderte übergeordnete Klasse mit der Änderung:

public class Rabbit {
    public String sex;
    public String name;

    public Rabbit(Builder builder) {
        sex = builder.sex;
        name = builder.name;
    }

    public static class Builder implements RabbitBuilder<Builder> {
        protected String sex;
        protected String name;

        public Builder() {}

        public Rabbit build() {
            return new Rabbit(this);
        }

        @Override
        public Builder sex(String sex) {
            this.sex = sex;
            return this;
        }

        @Override
        public Builder name(String name) {
            this.name = name;
            return this;
        }
    }
}

Die Kinderklasse

Die untergeordnete Klasse Builder muss dieselbe Schnittstelle (mit einem anderen generischen Typ) implementieren:

public static class LopBuilder implements RabbitBuilder<LopBuilder>

Innerhalb der Kindklasse Builder des Felds, das auf parentBuilder verweist:

private Rabbit.Builder baseBuilder;

dadurch wird sichergestellt, dass übergeordnete Builder-Methoden im Kind aufgerufen werden. Die Implementierung unterscheidet sich jedoch:

@Override
public LopBuilder sex(String sex) {
    baseBuilder.sex(sex);
    return this;
}

@Override
public LopBuilder name(String name) {
    baseBuilder.name(name);
    return this;
}

public Rabbit build() {
    return new Lop(this);
}

Der Erbauer von Builder:

public LopBuilder() {
    baseBuilder = new Rabbit.Builder();
}

Der Konstruktor der erstellten untergeordneten Klasse:

public Lop(LopBuilder builder) {
    super(builder.baseBuilder);
}
2
R. Zagórski

Diese Form scheint fast zu funktionieren. Es ist nicht sehr aufgeräumt, aber es sieht so aus, als würde es Ihre Probleme vermeiden:

class Rabbit<B extends Rabbit.Builder<B>> {

    String name;

    public Rabbit(Builder<B> builder) {
        this.name = builder.colour;
    }

    public static class Builder<B extends Rabbit.Builder<B>> {

        protected String colour;

        public B colour(String colour) {
            this.colour = colour;
            return (B)this;
        }

        public Rabbit<B> build () {
            return new Rabbit<>(this);
        }
    }
}

class Lop<B extends Lop.Builder<B>> extends Rabbit<B> {

    float earLength;

    public Lop(Builder<B> builder) {
        super(builder);
        this.earLength = builder.earLength;
    }

    public static class Builder<B extends Lop.Builder<B>> extends Rabbit.Builder<B> {

        protected float earLength;

        public B earLength(float earLength) {
            this.earLength = earLength;
            return (B)this;
        }

        @Override
        public Lop<B> build () {
            return new Lop<>(this);
        }
    }
}

public class Test {

    public void test() {
        Rabbit rabbit = new Rabbit.Builder<>().colour("White").build();
        Lop lop1 = new Lop.Builder<>().earLength(1.4F).colour("Brown").build();
        Lop lop2 = new Lop.Builder<>().colour("Brown").earLength(1.4F).build();
        //Lop.Builder<Lop, Lop.Builder> builder = new Lop.Builder<>();
    }

    public static void main(String args[]) {
        try {
            new Test().test();
        } catch (Throwable t) {
            t.printStackTrace(System.err);
        }
    }
}

Obwohl ich Rabbit und Lop (in beiden Formen) erfolgreich erstellt habe, kann ich an dieser Stelle nicht herausfinden, wie man tatsächlich ein Builder-Objekt mit seinem vollständigen Typ instanziiert.

Die Essenz dieser Methode beruht auf der Umwandlung in (B) in den Builder-Methoden. Auf diese Weise können Sie den Objekttyp und den Typ der Builder definieren und diese innerhalb des Objekts beibehalten, während es erstellt wird.

Wenn jemand die richtige Syntax dafür herausfinden könnte (was falsch ist), würde ich es schätzen.

Lop.Builder<Lop.Builder> builder = new Lop.Builder<>();
1
OldCurmudgeon

Mit dem gleichen Problem konfrontiert, verwendete ich die von emcmanus vorgeschlagene Lösung unter: https://community.Oracle.com/blogs/emcmanus/2010/10/24/using-builder-pattern-subclasses

Ich kopiere gerade seine/ihre bevorzugte Lösung hier. Angenommen, wir haben zwei Klassen: Shape und Rectangle. Rectangle erbt von Shape.

öffentliche Klasse Shape {

private final double opacity;

public double getOpacity() {
    return opacity;
}

protected static abstract class Init<T extends Init<T>> {
    private double opacity;

    protected abstract T self();

    public T opacity(double opacity) {
        this.opacity = opacity;
        return self();
    }

    public Shape build() {
        return new Shape(this);
    }
}

public static class Builder extends Init<Builder> {
    @Override
    protected Builder self() {
        return this;
    }
}

protected Shape(Init<?> init) {
    this.opacity = init.opacity;
}

}

Es gibt die innere Klasse Init, die abstrakt ist, und die innere Klasse Builder, die eine tatsächliche Implementierung darstellt. Wird bei der Implementierung von Rectangle hilfreich sein:

public class Rectangle erweitert Shape { private letzte doppelte Höhe;

public double getHeight() {
    return height;
}

protected static abstract class Init<T extends Init<T>> extends Shape.Init<T> {
    private double height;

    public T height(double height) {
        this.height = height;
        return self();
    }

    public Rectangle build() {
        return new Rectangle(this);
    }
}

public static class Builder extends Init<Builder> {
    @Override
    protected Builder self() {
        return this;
    }
}

protected Rectangle(Init<?> init) {
    super(init);
    this.height = init.height;
}

}

So instanziieren Sie die Rectangle:

new Rectangle.Builder().opacity(1.0D).height(1.0D).build();

Wieder eine abstrakte Init-Klasse, die von Shape.Init erbt, und eine Build-Klasse, die die eigentliche Implementierung darstellt. Jede Builder-Klasse implementiert die self-Methode, die dafür verantwortlich ist, eine korrekt gegossene Version von sich selbst zurückzugeben.

Shape.Init <-- Shape.Builder
     ^
     |
     |
Rectangle.Init <-- Rectangle.Builder
0
jmgonet

Ich habe ein wenig experimentiert und fand, dass dies für mich ganz gut funktioniert. Beachten Sie, dass ich es vorziehen sollte, die eigentliche Instanz am Anfang zu erstellen und alle Setter dieser Instanz aufzurufen. Dies ist nur eine Präferenz.

Der Hauptunterschied zur akzeptierten Antwort ist das 

  1. Ich übergebe einen Parameter, der den Rückgabetyp angibt
  2. Es gibt keine Notwendigkeit für eine Zusammenfassung ... und einen endgültigen Builder.
  3. Ich erstelle eine Convenience-Methode 'newBuilder'.

Der Code:

public class MySuper {
    private int superProperty;

    public MySuper() { }

    public void setSuperProperty(int superProperty) {
        this.superProperty = superProperty;
    }

    public static SuperBuilder<? extends MySuper, ? extends SuperBuilder> newBuilder() {
        return new SuperBuilder<>(new MySuper());
    }

    public static class SuperBuilder<R extends MySuper, B extends SuperBuilder<R, B>> {
        private final R mySuper;

        public SuperBuilder(R mySuper) {
            this.mySuper = mySuper;
        }

        public B withSuper(int value) {
            mySuper.setSuperProperty(value);
            return (B) this;
        }

        public R build() {
            return mySuper;
        }
    }
}

und dann sieht eine Unterklasse so aus:

public class MySub extends MySuper {
    int subProperty;

    public MySub() {
    }

    public void setSubProperty(int subProperty) {
        this.subProperty = subProperty;
    }

    public static SubBuilder<? extends MySub, ? extends SubBuilder> newBuilder() {
        return new SubBuilder(new MySub());
    }

    public static class SubBuilder<R extends MySub, B extends SubBuilder<R, B>>
        extends SuperBuilder<R, B> {

        private final R mySub;

        public SubBuilder(R mySub) {
            super(mySub);
            this.mySub = mySub;
        }

        public B withSub(int value) {
            mySub.setSubProperty(value);
            return (B) this;
        }
    }
}

und eine Subsub-Klasse

public class MySubSub extends MySub {
    private int subSubProperty;

    public MySubSub() {
    }

    public void setSubSubProperty(int subProperty) {
        this.subSubProperty = subProperty;
    }

    public static SubSubBuilder<? extends MySubSub, ? extends SubSubBuilder> newBuilder() {
        return new SubSubBuilder<>(new MySubSub());
    }

    public static class SubSubBuilder<R extends MySubSub, B extends SubSubBuilder<R, B>>
        extends SubBuilder<R, B> {

        private final R mySubSub;

        public SubSubBuilder(R mySub) {
            super(mySub);
            this.mySubSub = mySub;
        }

        public B withSubSub(int value) {
            mySubSub.setSubSubProperty(value);
            return (B)this;
        }
    }

}

Um zu überprüfen, ob es funktioniert, habe ich diesen Test verwendet:

MySubSub subSub = MySubSub
        .newBuilder()
        .withSuper (1)
        .withSub   (2)
        .withSubSub(3)
        .withSub   (2)
        .withSuper (1)
        .withSubSub(3)
        .withSuper (1)
        .withSub   (2)
        .build();
0
Niels Basjes

Der folgende IEEE-Konferenzbeitrag Refined Fluent Builder in Java bietet eine umfassende Lösung für das Problem. 

Es zerlegt die ursprüngliche Frage in zwei Unterprobleme von Vererbungsdefizit und Quasi-Invarianz und zeigt, wie sich eine Lösung für diese beiden Unterprobleme für die Vererbungsunterstützung mit Code-Wiederverwendung im klassischen Builder-Muster in Java öffnet.

0
mc00x1

Da Sie keine Generics verwenden können, ist es jetzt wahrscheinlich die Hauptaufgabe, das Tippen irgendwie zu lockern. Ich weiß nicht, wie Sie diese Eigenschaften anschließend verarbeiten, aber wenn Sie eine HashMap verwenden, um sie als Schlüssel-Wert-Paare zu speichern? Es gibt also nur eine einzige Wrapper-Methode (key, value) im Builder (oder der Builder ist möglicherweise nicht mehr erforderlich).

Der Nachteil wäre, dass bei der Verarbeitung der gespeicherten Daten weitere Typen verwendet werden.

Wenn dieser Fall zu locker ist, können Sie die vorhandenen Eigenschaften beibehalten, haben jedoch eine allgemeine Set-Methode, die die Reflektion verwendet und nach der Setter-Methode auf der Grundlage des Schlüsselnamens sucht. Obwohl ich denke, dass das Nachdenken ein Overkill wäre.

0
Samurai Girl

Die einfachste Lösung wäre, die Setter-Methoden der übergeordneten Klasse einfach zu überschreiben.

Sie vermeiden Generika, sind einfach zu bedienen, zu erweitern und zu verstehen, und Sie vermeiden Code-Duplizierungen, wenn Sie super.setter aufrufen.

public class Lop extends Rabbit {

    public final float earLength;
    public final String furColour;

    public Lop(final LopBuilder builder) {
        super(builder);
        this.earLength = builder.earLength;
        this.furColour = builder.furColour;
    }

    public static class LopBuilder extends Rabbit.Builder {

        protected float earLength;
        protected String furColour;

        public LopBuilder() {}

        @Override
        public LopBuilder sex(final String sex) {
            super.sex(sex);
            return this;
        }

        @Override
        public LopBuilder name(final String name) {
            super.name(name);
            return this;
        }

        public LopBuilder earLength(final float length) {
            this.earLength = length;
            return this;
        }

        public LopBuilder furColour(final String colour) {
            this.furColour = colour;
            return this;
        }

        @Override
        public Lop build() {
            return new Lop(this);
        }
    }
}
0
benez