Подтвердить что ты не робот

Шаблон и наследование строителя

У меня есть иерархия объектов, которая усложняется по мере углубления дерева наследования. Ни один из них не является абстрактным, следовательно, все их экземпляры служат более или менее сложной цели.

Поскольку количество параметров достаточно велико, я бы хотел использовать шаблон Builder для установки свойств, а не кодировать несколько конструкторов. Поскольку мне нужно удовлетворить все перестановки, листовые классы в моем дереве наследования будут иметь телескопические конструкторы.

Я искал ответ здесь, когда столкнулся с некоторыми проблемами во время моего дизайна. Прежде всего, позвольте мне привести простой, поверхностный пример, иллюстрирующий проблему.

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

Теперь, когда у нас есть код для продолжения, я хочу построить Lop:

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

Этот вызов не будет компилироваться, так как последний цепочечный вызов не может быть решен, при этом Builder не определяет метод earLength. Таким образом, этот способ требует, чтобы все вызовы были связаны в определенном порядке, что очень непрактично, особенно с глубоким иерархическим деревом.

Теперь, во время поиска ответа, я натолкнулся на создание подкласса класса Java Builder, в котором предлагается использовать шаблон Curiously Recursive Generic. Однако, поскольку моя иерархия не содержит абстрактный класс, это решение не будет работать для меня. Но подход основан на абстракции и полиморфизме, поэтому я не верю, что смогу адаптировать его к своим потребностям.

Подход, который я сейчас выбрал, состоит в том, чтобы переопределить все методы Builder суперкласса в иерархии и просто сделать следующее:

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

При таком подходе я могу заверить меня, что мне возвращают экземпляр, на котором я могу выполнять цепные вызовы. Хотя это и не так плохо, как "Телескопический анти-паттерн", это вторая секунда, и я считаю это немного "хакерским".

Есть ли другое решение моей проблемы, о котором я не знаю? Желательно, чтобы решение соответствовало шаблону дизайна. Спасибо!

4b9b3361

Ответ 1

Это, конечно, возможно с рекурсивным ограничением, но компоновщики подтипов также должны быть универсальными, и вам нужно несколько промежуточных абстрактных классов. Это немного громоздко, но все же проще, чем неуниверсальная версия.

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

Есть способ избежать "конкретных" листовых классов, где, если бы у нас было это:

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

Затем вам нужно создать новые экземпляры с ромбом и использовать подстановочные знаки в ссылочном типе:

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

Это работает, потому что ограничение переменной типа гарантирует, что все методы, например, RabbitBuilder имеют тип возврата с RabbitBuilder, даже если аргумент типа является просто подстановочным знаком.

Я не большой поклонник этого, хотя, потому что вам нужно везде использовать подстановочные знаки, и вы можете только создать новый экземпляр, используя алмаз или необработанный тип. Я полагаю, что в любом случае вы получите немного неловкости.


И, кстати, об этом:

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

Есть способ избежать этого непроверенного приведения, который состоит в том, чтобы сделать метод абстрактным:

abstract B self();

И затем переопределите это в подклассе листа:

@Override
RabbitBuilder self() { return this; }

Проблема с этим заключается в том, что, хотя он более безопасен для типов, подкласс может возвращать что-то, кроме this. По сути, в любом случае у подкласса есть возможность сделать что-то не так, поэтому я не вижу особой причины для предпочтения одного из этих подходов другому.

Ответ 2

Если кто-то все еще столкнулся с одной и той же проблемой, я предлагаю следующее решение, которое соответствует шаблону проектирования "Предпочитаю композицию над наследованием".

Родительский класс

Основным элементом этого является интерфейс, который должен реализовывать родительский класс Builder:

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

Вот измененный родительский класс с изменением:

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

Детский класс

Детский класс Builder должен реализовывать один и тот же интерфейс (с разным общим типом):

public static class LopBuilder implements RabbitBuilder<LopBuilder>

Внутри дочернего класса Builder поле, ссылающееся на родительский Builder:

private Rabbit.Builder baseBuilder;

это гарантирует, что родительские методы Builder вызываются в дочернем элементе, однако их реализация различна:

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

Конструктор Builder:

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

Конструктор построенного дочернего класса:

public Lop(LopBuilder builder) {
    super(builder.baseBuilder);
}

Ответ 3

Эта форма, похоже, почти работает. Это не очень аккуратно, но похоже, что это позволяет избежать проблем:

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

Хотя я успешно построил Rabbit и Lop (в обеих формах), я не могу на этом этапе решить, как фактически создать экземпляр одного из объектов Builder с полным типом.

Суть этого метода основана на применении метода (B) в методах Builder. Это позволяет вам определить тип объекта и тип Builder и сохранить его внутри объекта во время его построения.

Если кто-то мог бы выработать правильный синтаксис для этого (что неправильно), я был бы признателен.

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

Ответ 4

Поскольку вы не можете использовать generics, теперь, вероятно, главная задача - как-то ослабить типизацию. Я не знаю, как вы обрабатываете эти свойства впоследствии, но что, если вы использовали HashMap для хранения их в качестве пар ключ-значение? Таким образом, в компоновщике будет только один метод (ключ, значение) обертки (или, возможно, больше не понадобится компоновщик).

Недостатком является обработка дополнительных типов при обработке сохраненных данных.

Если этот случай слишком свободен, вы можете сохранить существующие свойства, но иметь общий метод набора, который использует отражение и поиск метода setter на основе имени "key". Хотя я думаю, что отражение было бы излишним.

Ответ 5

Я немного экспериментировал, и я нашел, что это очень хорошо работает для меня. Обратите внимание, что я предпочитаю создавать фактический экземпляр в начале и вызывать все настройки в этом экземпляре. Это только предпочтение.

Основные отличия от принятого ответа заключаются в том, что

  • Я передаю параметр, указывающий тип возврата
  • Нет необходимости в абстрактном... и окончательном построителе.
  • Я создаю удобный метод 'newBuilder'.

Код:

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

а затем подкласс выглядит следующим образом:

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

и подпункт класса

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

}

Чтобы проверить это полностью, я использовал этот тест:

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

Ответ 6

Наиболее простым решением было бы просто переопределить методы установки родительского класса.

Вы избегаете обобщений, их легко использовать, расширять и понимать, а также избегаете дублирования кода при вызове super.setter.

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

Ответ 7

Столкнувшись с той же проблемой, я использовал решение, предложенное emcmanus по адресу: https://community.oracle.com/blogs/emcmanus/2010/10/24/using-builder-pattern-subclasses

Я просто переписываю его/ее предпочтительное решение здесь. Допустим, у нас есть два класса, Shape и Rectangle. Rectangle наследует от 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;
    }
}

Существует внутренний класс Init, который является абстрактным, и внутренний класс Builder, который является реальной реализацией. Будет полезно при реализации 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;
    }
}

Чтобы создать экземпляр Rectangle:

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

Опять же, абстрактный класс Init, унаследованный от Shape.Init, и Build который является фактической реализацией. Каждый класс Builder реализует метод self, который отвечает за возвращение правильно приведенной версии самого себя.

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

Ответ 8

Следующий вклад в конференцию IEEE Refined Fluent Builder на Java дает исчерпывающее решение проблемы.

Он разбивает исходный вопрос на две подзадачи: дефицит наследования и квазиинвариантность и показывает, как решение этих двух подзадач открывает поддержку наследования с повторным использованием кода в классическом шаблоне компоновщика в Java.