Wzór budowniczego i dziedziczenie

Mam hierarchię obiektów, która zwiększa złożoność w miarę pogłębiania się drzewa dziedziczenia. Żadna z nich nie jest abstrakcyjna, stąd wszystkie ich instancje służą mniej lub bardziej wyrafinowanemu celowi.

Ponieważ liczba parametrów jest dość wysoka, chciałbym użyć wzorca Builder do ustawiania właściwości, a nie kodowania kilku konstruktorów. Ponieważ muszę zaspokoić wszystkie permutacje, klasy liści w moim drzewie dziedziczenia będą miały teleskopowe konstruktory.

Przeglądałem dla odpowiedz tutaj, gdy napotkałem pewne problemy podczas mojego projektowania. Po pierwsze, pozwól, że dam ci prosty, płytki przykład, aby zilustrować problem.

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);
        }
    }
}

Teraz, gdy mamy jakiś kod do zrobienia, imaging chcę zbudować Lop:

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

To wywołanie nie zostanie skompilowane, ponieważ nie można rozwiązać ostatniego wywołania łańcuchowego, Builder nie definiując metody earLength. Tak więc w ten sposób wszystkie wywołania muszą być połączone łańcuchowo w określonej kolejności, która jest bardzo niepraktyczna, zwłaszcza w głębokim drzewie hierarchii.

Teraz, podczas poszukiwań odpowiedzi natknąłem się na Podklasowanie klasy Java Builder, która sugeruje użycie ciekawie rekurencyjnego generycznego wzorca. Ponieważ jednak moja hierarchia nie zawiera abstrakcyjnej klasy, To rozwiązanie nie będzie dla mnie skuteczne. Ale podejście opiera się na abstrakcji i polimorfizmie, aby funkcjonować, dlatego nie wierzę, że mogę dostosować go do moich potrzeb.

Podejście, z którym obecnie się rozstrzygnąłem, polega na nadpisaniu wszystkich metod klasy nadrzędnej Builder w hierarchii i po prostu wykonaj następujące czynności:

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

Dzięki temu podejściu mogę zapewnić, że zwracam instancję, na którą mogę wysyłać wywołania łańcuchowe. Chociaż nie jest to tak gorsze, jak teleskopowy anty-pattern, jest to bliska sekunda i uważam go za nieco "hakowaty".

Czy jest inne rozwiązanie mojego problemu, którego nie jestem świadomy? Najlepiej rozwiązanie zgodne ze wzorcem projektowym. Dziękuję!

Author: Community, 2014-01-13

8 answers

Jest to z pewnością możliwe przy powiązaniu rekurencyjnym, ale konstruktorzy podtypów muszą być również ogólnikowymi i potrzebujesz kilku tymczasowych klas abstrakcyjnych. Jest to trochę uciążliwe, ale nadal jest łatwiejsze niż wersja nie-ogólna.

/**
 * 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);
    }
}

Jest sposób, aby uniknąć "betonowych" klas liścia, gdzie gdybyśmy mieli to:

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

Następnie musisz utworzyć nowe instancje za pomocą diamentu i użyć symboli wieloznacznych w typie odniesienia:

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

To działa, ponieważ związany zmienna type zapewnia, że wszystkie metody np. {[6] } mają zwracany typ z RabbitBuilder, nawet jeśli argument type jest tylko znakiem wieloznacznym.

Nie przepadam za tym, ponieważ wszędzie trzeba używać symboli wieloznacznych, a nową instancję można utworzyć tylko za pomocą diamentu lub typu raw. Przypuszczam, że i tak kończysz z małą niezręcznością.


A tak przy okazji, o tym:

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

Jest sposób na uniknięcie tego niezaangażowanego rzutu, który jest, aby metoda była abstrakcyjna:

abstract B self();

A następnie nadpisać go w podklasie leaf:

@Override
RabbitBuilder self() { return this; }

Problem z robieniem tego w ten sposób polega na tym, że chociaż jest to bardziej bezpieczne dla typu, podklasa może zwrócić coś innego niż this. Zasadniczo, tak czy inaczej, podklasa ma możliwość zrobienia czegoś złego, więc nie widzę powodu, aby preferować jedno z tych podejść nad drugim.

 45
Author: Radiodef,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-09-12 14:37:13

Jeśli ktoś jeszcze wpadł na ten sam problem, proponuję następujące rozwiązanie, które odpowiada wzorcowi projektowemu "preferuj kompozycję niż dziedziczenie".

Klasa rodzica

Głównym jego elementem jest interfejs, który musi zaimplementować konstruktor klasy nadrzędnej:

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

Oto zmieniona Klasa rodzica ze zmianą:

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;
        }
    }
}

Klasa dziecięca

Klasa potomna Builder musi zaimplementować ten sam interfejs (z różnymi rodzaj ogólny):

public static class LopBuilder implements RabbitBuilder<LopBuilder>

Wewnątrz klasy potomnej Builder pole odwołujące się do rodzica Builder:

private Rabbit.Builder baseBuilder;

Gwarantuje to, że metody rodzica Builder są wywoływane w dziecku, jednak ich implementacja jest inna:

@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);
}

Konstruktor konstruktora:

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

Konstruktor zbudowanej klasy dziecka:

public Lop(LopBuilder builder) {
    super(builder.baseBuilder);
}
 2
Author: R. Zagórski,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-03-28 15:57:42

Ta forma wydaje się prawie działać. Nie jest zbyt schludny, ale wygląda na to, że unika twoich problemów: {]}

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);
        }
    }
}

Chociaż udało mi się zbudować Rabbit i Lop (w obu formach), nie mogę na tym etapie dowiedzieć się, jak faktycznie utworzyć instancję jednego z obiektów Builder z pełnym typem.

Istota tej metody opiera się na rzucaniu do (B) w metodach Builder. Pozwala to zdefiniować typ obiektu i typ Builder i zachować to wewnątrz obiektu, gdy jest skonstruowany.

Jeśli ktoś mógłby wypracować poprawną składnię tego (co jest złe) byłbym wdzięczny.

Lop.Builder<Lop.Builder> builder = new Lop.Builder<>();
 1
Author: OldCurmudgeon,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-01-14 09:03:18

Ponieważ nie można używać generyków, teraz prawdopodobnie głównym zadaniem jest jakoś poluzować pisanie. Nie wiem, jak później przetwarzasz te właściwości, ale co, jeśli użyłeś Hashmapy do przechowywania ich jako par klucz-wartość? W konstruktorze będzie więc tylko jedna metoda owijania zestawu(klucz, wartość) (lub konstruktor może już nie być konieczny).

Minusem byłyby dodatkowe odlewy typu podczas przetwarzania przechowywanych danych.

Jeśli ta sprawa jest zbyt luźna, możesz zachować istniejące właściwości, ale mają ogólną metodę set, która wykorzystuje odbicie i wyszukuje metodę setter na podstawie nazwy 'key'. Chociaż myślę, że refleksja byłaby przesadą.

 0
Author: Samurai Girl,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-01-13 10:14:50

Przeprowadziłem kilka eksperymentów i okazało się, że to działa całkiem nieźle dla mnie. Zauważ, że wolę utworzyć rzeczywistą instancję na początku i wywołać wszystkie setery na tej instancji. To tylko Preferencje.

Główne różnice z zaakceptowaną odpowiedzią są takie, że

  1. przekazuję parametr wskazujący Typ powrotu
  2. nie ma potrzeby abstrakcji... i ostatecznym budowniczym.
  3. tworzę wygodną metodę 'newBuilder'.

The kod:

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;
        }
    }
}

I wtedy podklasa wygląda tak:

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;
        }
    }
}

Oraz podklasa

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;
        }
    }

}

Aby zweryfikować to w pełni działa użyłem tego testu:

MySubSub subSub = MySubSub
        .newBuilder()
        .withSuper (1)
        .withSub   (2)
        .withSubSub(3)
        .withSub   (2)
        .withSuper (1)
        .withSubSub(3)
        .withSuper (1)
        .withSub   (2)
        .build();
 0
Author: Niels Basjes,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-09-04 11:41:42

Najprostszą poprawką byłoby po prostu nadpisanie metod settera klasy nadrzędnej.

Unikasz generyków, jest łatwy w użyciu, rozszerzony i zrozumiały, a także unikasz duplikacji kodu podczas wywoływania super.seter.

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
Author: benez,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-08-14 13:46:38

Skonfrontowany z tym samym problemem, skorzystałem z rozwiązania zaproponowanego przez emcmanus na: https://community.oracle.com/blogs/emcmanus/2010/10/24/using-builder-pattern-subclasses

Po prostu przepisuję jego / jej preferowane rozwiązanie. Powiedzmy, że mamy dwie klasy, Shape i Rectangle. Rectangle dziedziczy z Shape.

Public class 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;
}

}

Istnieje Init klasa wewnętrzna, która jest abstrakcyjna, oraz Builder Klasa wewnętrzna, która jest rzeczywistą wdrożenie. Będzie przydatny przy implementacji Rectangle:

Public Class Rectangle extends Shape { private final double height;

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;
}

}

Aby utworzyć instancję Rectangle:

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

Ponownie, klasa abstrakcyjna Init, dziedzicząca z Shape.Init i Build, która jest rzeczywistą implementacją. Każda klasa Builder implementuje metodę self, która jest odpowiedzialna za zwrócenie poprawnie oddanej jej wersji.

Shape.Init <-- Shape.Builder
     ^
     |
     |
Rectangle.Init <-- Rectangle.Builder
 0
Author: jmgonet,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-09-12 12:01:44

Poniższy wkład konferencji IEEE dopracowany Fluent Builder w Javie daje kompleksowe rozwiązanie problemu.

Analizuje pierwotne pytanie na dwa pod-problemy niedoboru dziedziczenia i quasi niezmienniczości i pokazuje, w jaki sposób rozwiązanie tych dwóch pod-problemów otwiera się dla wsparcia dziedziczenia za pomocą kodu ponownego użycia w klasycznym wzorze konstruktora w Javie.

 0
Author: mc00x1,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-10-01 15:05:44