Создание подкласса класса Java Builder


133

Расскажите об этой статье доктора Доббса и, в частности, о шаблоне Builder, как нам поступить в случае создания подкласса от Builder? Если взять урезанную версию примера, в котором мы хотим создать подкласс для добавления маркировки ГМО, наивная реализация будет выглядеть так:

public class NutritionFacts {                                                                                                    

    private final int calories;                                                                                                  

    public static class Builder {                                                                                                
        private int calories = 0;                                                                                                

        public Builder() {}                                                                                                      

        public Builder calories(int val) { calories = val; return this; }                                                                                                                        

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

    protected NutritionFacts(Builder builder) {                                                                                  
        calories = builder.calories;                                                                                             
    }                                                                                                                            
}

Подкласс:

public class GMOFacts extends NutritionFacts {                                                                                   

    private final boolean hasGMO;                                                                                                

    public static class Builder extends NutritionFacts.Builder {                                                                 

        private boolean hasGMO = false;                                                                                          

        public Builder() {}                                                                                                      

        public Builder GMO(boolean val) { hasGMO = val; return this; }                                                           

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

    protected GMOFacts(Builder builder) {                                                                                        
        super(builder);                                                                                                          
        hasGMO = builder.hasGMO;                                                                                                 
    }                                                                                                                            
}

Теперь мы можем написать такой код:

GMOFacts.Builder b = new GMOFacts.Builder();
b.GMO(true).calories(100);

Но, если мы сделаем неправильный порядок, все не удастся:

GMOFacts.Builder b = new GMOFacts.Builder();
b.calories(100).GMO(true);

Проблема, конечно, в том, что NutritionFacts.Builderвозвращает a NutritionFacts.Builder, а не a GMOFacts.Builder, так как же нам решить эту проблему, или есть лучший шаблон для использования?

Примечание: этот ответ на аналогичный вопрос предлагает классы, которые у меня есть выше; мой вопрос касается проблемы обеспечения правильного порядка вызовов строителя.


1
Я думаю, что следующая ссылка описывает хороший подход: egalluzzo.blogspot.co.at/2010/06/…
stuXnet

1
Но как вам build()на выходе b.GMO(true).calories(100)?
Шридхар Сарнобат

Ответы:


170

Вы можете решить эту проблему с помощью дженериков. Я думаю, это называется «любопытно повторяющиеся общие шаблоны».

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

public class NutritionFacts {

    private final int calories;

    public static class Builder<T extends Builder<T>> {

        private int calories = 0;

        public Builder() {}

        public T calories(int val) {
            calories = val;
            return (T) this;
        }

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

    protected NutritionFacts(Builder<?> builder) {
        calories = builder.calories;
    }
}

Теперь создайте экземпляр базового построителя с построителем производного класса в качестве универсального аргумента.

public class GMOFacts extends NutritionFacts {

    private final boolean hasGMO;

    public static class Builder extends NutritionFacts.Builder<Builder> {

        private boolean hasGMO = false;

        public Builder() {}

        public Builder GMO(boolean val) {
            hasGMO = val;
            return this;
        }

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

    protected GMOFacts(Builder builder) {
        super(builder);
        hasGMO = builder.hasGMO;
    }
}

2
Хм, думаю, мне придется либо (а) опубликовать новый вопрос, (б) изменить дизайн с помощью implementsвместо extends, или (в) все выбросить. У меня странная ошибка компиляции, где leafBuilder.leaf().leaf()и leafBuilder.mid().leaf()все в порядке, но leafBuilder.leaf().mid().leaf()не удается ...
Ken YN

11
@gkamal return (T) this;приводит к unchecked or unsafe operationsпредупреждению. Этого невозможно избежать, правда?
Дмитрий Миньковский

5
Чтобы устранить unchecked castпредупреждение, см. Предлагаемое ниже решение среди других ответов: stackoverflow.com/a/34741836/3114959
Степан Вавра

8
Обратите внимание, что Builder<T extends Builder>на самом деле это rawtype - так и должно быть Builder<T extends Builder<T>>.
Boris the Spider

2
@ user2957378 Builderfor GMOFactsтакже должен быть общим Builder<B extends Builder<B>> extends NutritionFacts.Builder<Builder>- и этот шаблон может продолжаться на столько уровней, сколько потребуется. Если вы объявите неуниверсальный конструктор, вы не сможете расширить шаблон.
Boris the Spider

44

Просто для записи, чтобы избавиться от

unchecked or unsafe operations предупреждение

для return (T) this;утверждения, о котором говорят @dimadima и @Thomas N., в некоторых случаях применяется следующее решение.

Создайте abstractконструктор, который объявляет универсальный тип ( T extends Builderв данном случае) и объявляет protected abstract T getThis()абстрактный метод следующим образом:

public abstract static class Builder<T extends Builder<T>> {

    private int calories = 0;

    public Builder() {}

    /** The solution for the unchecked cast warning. */
    public abstract T getThis();

    public T calories(int val) {
        calories = val;

        // no cast needed
        return getThis();
    }

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

См. Http://www.angelikalanger.com/GenericsFAQ/FAQSections/ProgrammingIdioms.html#FAQ205 для получения дополнительных сведений.


Почему build()метод возвращает NutrutionFacts здесь?
mvd

@mvd Потому что это ответ на вопрос? В подтипах вы его переопределите, напримерpublic GMOFacts build() { return new GMOFacts(this); }
Степан Вавра

Проблема возникает, когда мы хотим добавить второго ребенка, BuilderC extends BuilderBа BuilderB extends BuilderAкогда BuilderBнетabstract
2016,

1
Это не ответ на вопрос, потому что базовый класс не может быть абстрактным!
Роланд

«Сделайте абстрактным построитель, который объявляет общий тип» - что, если бы я хотел использовать этот построитель напрямую?
Дейзи

21

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

public abstract class TopLevel {
    protected int foo;
    protected TopLevel() {
    }
    protected static abstract class Builder
        <T extends TopLevel, B extends Builder<T, B>> {
        protected T object;
        protected B thisObject;
        protected abstract T createObject();
        protected abstract B thisObject();
        public Builder() {
            object = createObject();
            thisObject = thisObject();
        }
        public B foo(int foo) {
            object.foo = foo;
            return thisObject;
        }
        public T build() {
            return object;
        }
    }
}

Затем у вас есть промежуточный класс, который расширяет этот класс и его конструктор, и еще столько, сколько вам нужно:

public abstract class SecondLevel extends TopLevel {
    protected int bar;
    protected static abstract class Builder
        <T extends SecondLevel, B extends Builder<T, B>> extends TopLevel.Builder<T, B> {
        public B bar(int bar) {
            object.bar = bar;
            return thisObject;
        }
    }
}

И, наконец, конкретный листовой класс, который может вызывать все методы построителя для любого из своих родителей в любом порядке:

public final class LeafClass extends SecondLevel {
    private int baz;
    public static final class Builder extends SecondLevel.Builder<LeafClass,Builder> {
        protected LeafClass createObject() {
            return new LeafClass();
        }
        protected Builder thisObject() {
            return this;
        }
        public Builder baz(int baz) {
            object.baz = baz;
            return thisObject;
        }
    }
}

Затем вы можете вызывать методы в любом порядке из любого класса в иерархии:

public class Demo {
    LeafClass leaf = new LeafClass.Builder().baz(2).foo(1).bar(3).build();
}

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

Обратите внимание, что класс Builder в LeafClass не следует тому же <T extends SomeClass, B extends SomeClass.Builder<T,B>> extends SomeClassParent.Builder<T,B>шаблону, что и промежуточный класс SecondLevel, вместо этого он объявляет определенные типы. Вы не можете создать класс, пока не дойдете до листа, используя определенные типы, но как только вы это сделаете, вы не сможете его расширить, потому что вы используете определенные типы и отказались от шаблона Curious Recurring Template Pattern. Эта ссылка может помочь: angelikalanger.com/GenericsFAQ/FAQSections/…
Q23,

7

Вы также можете переопределить calories()метод и позволить ему вернуть расширяющийся построитель. Это компилируется, потому что Java поддерживает ковариантные возвращаемые типы .

public class GMOFacts extends NutritionFacts {
    private final boolean hasGMO;
    public static class Builder extends NutritionFacts.Builder {
        private boolean hasGMO = false;
        public Builder() {
        }
        public Builder GMO(boolean val)
        { hasGMO = val; return this; }
        public Builder calories(int val)
        { super.calories(val); return this; }
        public GMOFacts build() {
            return new GMOFacts(this);
        }
    }
    [...]
}

Ах, я этого не знал, так как у меня опыт работы на C ++. Это полезный подход для этого небольшого примера, но с полноценным классом повторение всех методов становится болью, и при этом болью, подверженной ошибкам. +1 за то, что научил меня чему-то новому!
Ken YN

Мне кажется, это ничего не решает. Причина (IMO) для подкласса родителя состоит в том, чтобы повторно использовать родительские методы без их переопределения. Если классы являются просто объектами значений без реальной логики в методах построителя, за исключением установки простого значения, то вызов родительского метода в методе переопределения практически не имеет значения.
Developer Dude

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

3

Существует также другой способ создания классов по Builderшаблону, который соответствует принципу «Предпочитать композицию наследованию».

Определите интерфейс, который Builderнаследует родительский класс :

public interface FactsBuilder<T> {

    public T calories(int val);
}

Реализация NutritionFactsпочти такая же (за исключением Builderреализации интерфейса FactsBuilder):

public class NutritionFacts {

    private final int calories;

    public static class Builder implements FactsBuilder<Builder> {
        private int calories = 0;

        public Builder() {
        }

        @Override
        public Builder calories(int val) {
            return this;
        }

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

    protected NutritionFacts(Builder builder) {
        calories = builder.calories;
    }
}

Класс Builderдочернего класса должен расширять тот же интерфейс (за исключением другой общей реализации):

public static class Builder implements FactsBuilder<Builder> {
    NutritionFacts.Builder baseBuilder;

    private boolean hasGMO = false;

    public Builder() {
        baseBuilder = new NutritionFacts.Builder();
    }

    public Builder GMO(boolean val) {
        hasGMO = val;
        return this;
    }

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

    @Override
    public Builder calories(int val) {
        baseBuilder.calories(val);
        return this;
    }
}

Обратите внимание, NutritionFacts.Builderэто поле внутри GMOFacts.Builder(называется baseBuilder). Метод, реализованный из FactsBuilderинтерфейса, вызывает baseBuilderодноименный метод:

@Override
public Builder calories(int val) {
    baseBuilder.calories(val);
    return this;
}

Также произошли большие изменения в конструкторе GMOFacts(Builder builder). Первый вызов в конструкторе конструктора родительского класса должен передать соответствующие NutritionFacts.Builder:

protected GMOFacts(Builder builder) {
    super(builder.baseBuilder);
    hasGMO = builder.hasGMO;
}

Полная реализация GMOFactsкласса:

public class GMOFacts extends NutritionFacts {

    private final boolean hasGMO;

    public static class Builder implements FactsBuilder<Builder> {
        NutritionFacts.Builder baseBuilder;

        private boolean hasGMO = false;

        public Builder() {
        }

        public Builder GMO(boolean val) {
            hasGMO = val;
            return this;
        }

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

        @Override
        public Builder calories(int val) {
            baseBuilder.calories(val);
            return this;
        }
    }

    protected GMOFacts(Builder builder) {
        super(builder.baseBuilder);
        hasGMO = builder.hasGMO;
    }
}

3

Полный трехуровневый пример наследования множественных построителей будет выглядеть так :

(Для версии с конструктором копирования для построителя см. Второй пример ниже)

Первый уровень - родительский (потенциально абстрактный)

import lombok.ToString;

@ToString
@SuppressWarnings("unchecked")
public abstract class Class1 {
    protected int f1;

    public static class Builder<C extends Class1, B extends Builder<C, B>> {
        C obj;

        protected Builder(C constructedObj) {
            this.obj = constructedObj;
        }

        B f1(int f1) {
            obj.f1 = f1;
            return (B)this;
        }

        C build() {
            return obj;
        }
    }
}

Второй уровень

import lombok.ToString;

@ToString(callSuper=true)
@SuppressWarnings("unchecked")
public class Class2 extends Class1 {
    protected int f2;

    public static class Builder<C extends Class2, B extends Builder<C, B>> extends Class1.Builder<C, B> {
        public Builder() {
            this((C) new Class2());
        }

        protected Builder(C obj) {
            super(obj);
        }

        B f2(int f2) {
            obj.f2 = f2;
            return (B)this;
        }
    }
}

Третий уровень

import lombok.ToString;

@ToString(callSuper=true)
@SuppressWarnings("unchecked")
public class Class3 extends Class2 {
    protected int f3;

    public static class Builder<C extends Class3, B extends Builder<C, B>> extends Class2.Builder<C, B> {
        public Builder() {
            this((C) new Class3());
        }

        protected Builder(C obj) {
            super(obj);
        }

        B f3(int f3) {
            obj.f3 = f3;
            return (B)this;
        }
    }
}

И пример использования

public class Test {
    public static void main(String[] args) {
        Class2 b1 = new Class2.Builder<>().f1(1).f2(2).build();
        System.out.println(b1);
        Class2 b2 = new Class2.Builder<>().f2(2).f1(1).build();
        System.out.println(b2);

        Class3 c1 = new Class3.Builder<>().f1(1).f2(2).f3(3).build();
        System.out.println(c1);
        Class3 c2 = new Class3.Builder<>().f3(3).f1(1).f2(2).build();
        System.out.println(c2);
        Class3 c3 = new Class3.Builder<>().f3(3).f2(2).f1(1).build();
        System.out.println(c3);
        Class3 c4 = new Class3.Builder<>().f2(2).f3(3).f1(1).build();
        System.out.println(c4);
    }
}


Немного более длинная версия с конструктором копирования для конструктора:

Первый уровень - родительский (потенциально абстрактный)

import lombok.ToString;

@ToString
@SuppressWarnings("unchecked")
public abstract class Class1 {
    protected int f1;

    public static class Builder<C extends Class1, B extends Builder<C, B>> {
        C obj;

        protected void setObj(C obj) {
            this.obj = obj;
        }

        protected void copy(C obj) {
            this.f1(obj.f1);
        }

        B f1(int f1) {
            obj.f1 = f1;
            return (B)this;
        }

        C build() {
            return obj;
        }
    }
}

Второй уровень

import lombok.ToString;

@ToString(callSuper=true)
@SuppressWarnings("unchecked")
public class Class2 extends Class1 {
    protected int f2;

    public static class Builder<C extends Class2, B extends Builder<C, B>> extends Class1.Builder<C, B> {
        public Builder() {
            setObj((C) new Class2());
        }

        public Builder(C obj) {
            this();
            copy(obj);
        }

        @Override
        protected void copy(C obj) {
            super.copy(obj);
            this.f2(obj.f2);
        }

        B f2(int f2) {
            obj.f2 = f2;
            return (B)this;
        }
    }
}

Третий уровень

import lombok.ToString;

@ToString(callSuper=true)
@SuppressWarnings("unchecked")
public class Class3 extends Class2 {
    protected int f3;

    public static class Builder<C extends Class3, B extends Builder<C, B>> extends Class2.Builder<C, B> {
        public Builder() {
            setObj((C) new Class3());
        }

        public Builder(C obj) {
            this();
            copy(obj);
        }

        @Override
        protected void copy(C obj) {
            super.copy(obj);
            this.f3(obj.f3);
        }

        B f3(int f3) {
            obj.f3 = f3;
            return (B)this;
        }
    }
}

И пример использования

public class Test {
    public static void main(String[] args) {
        Class3 c4 = new Class3.Builder<>().f2(2).f3(3).f1(1).build();
        System.out.println(c4);

        // Class3 builder copy
        Class3 c42 = new Class3.Builder<>(c4).f2(12).build();
        System.out.println(c42);
        Class3 c43 = new Class3.Builder<>(c42).f2(22).f1(11).build();
        System.out.println(c43);
        Class3 c44 = new Class3.Builder<>(c43).f3(13).f1(21).build();
        System.out.println(c44);
    }
}

2

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

public class TestInheritanceBuilder {
  public static void main(String[] args) {
    SubType.Builder builder = new SubType.Builder();
    builder.withFoo("FOO").withBar("BAR").withBaz("BAZ");
    SubType st = builder.build();
    System.out.println(st.toString());
    builder.withFoo("BOOM!").withBar("not getting here").withBaz("or here");
  }
}

поддерживается

public class SubType extends ParentType {
  String baz;
  protected SubType() {}

  public static class Builder extends ParentType.Builder {
    private SubType object = new SubType();

    public Builder withBaz(String baz) {
      getObject().baz = baz;
      return this;
    }

    public Builder withBar(String bar) {
      super.withBar(bar);
      return this;
    }

    public Builder withFoo(String foo) {
      super.withFoo(foo);
      return this;
    }

    public SubType build() {
      // or clone or copy constructor if you want to stamp out multiple instances...
      SubType tmp = getObject();
      setObject(new SubType());
      return tmp;
    }

    protected SubType getObject() {
      return object;
    }

    private void setObject(SubType object) {
      this.object = object;
    }
  }

  public String toString() {
    return "SubType2{" +
        "baz='" + baz + '\'' +
        "} " + super.toString();
  }
}

и родительский тип:

public class ParentType {
  String foo;
  String bar;

  protected ParentType() {}

  public static class Builder {
    private ParentType object = new ParentType();

    public ParentType object() {
      return getObject();
    }

    public Builder withFoo(String foo) {
      if (!"foo".equalsIgnoreCase(foo)) throw new IllegalArgumentException();
      getObject().foo = foo;
      return this;
    }

    public Builder withBar(String bar) {
      getObject().bar = bar;
      return this;
    }

    protected ParentType getObject() {
      return object;
    }

    private void setObject(ParentType object) {
      this.object = object;
    }

    public ParentType build() {
      // or clone or copy constructor if you want to stamp out multiple instances...
      ParentType tmp = getObject();
      setObject(new ParentType());
      return tmp;
    }
  }

  public String toString() {
    return "ParentType2{" +
        "foo='" + foo + '\'' +
        ", bar='" + bar + '\'' +
        '}';
  }
}

Ключевые моменты:

  • Инкапсулируйте объект в построителе, чтобы наследование не позволяло вам установить поле для объекта, содержащегося в родительском типе.
  • Вызов super гарантирует, что логика (если таковая имеется), добавленная к методам построения супертипов, сохраняется в подтипах.
  • Нижняя сторона - это создание ложного объекта в родительском классе (ах) ... Но см. Ниже способ очистки этого
  • Верхняя сторона намного легче понять с первого взгляда, и нет подробного конструктора, передающего свойства.
  • Если у вас есть несколько потоков, обращающихся к вашим объектам построителя ... Думаю, я рад, что я не вы :).

РЕДАКТИРОВАТЬ:

Я нашел способ обойти создание ложного объекта. Сначала добавьте это к каждому строителю:

private Class whoAmI() {
  return new Object(){}.getClass().getEnclosingMethod().getDeclaringClass();
}

Затем в конструкторе для каждого строителя:

  if (whoAmI() == this.getClass()) {
    this.obj = new ObjectToBuild();
  }

Стоимость - это дополнительный файл класса для new Object(){}анонимного внутреннего класса.


1

Вы могли бы создать статический фабричный метод в каждом из ваших классов:

NutritionFacts.newBuilder()
GMOFacts.newBuilder()

Затем этот статический фабричный метод вернет соответствующий построитель. Вы можете GMOFacts.Builderрасширить a NutritionFacts.Builder, это не проблема. Проблема здесь будет в видимости ...


0

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

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


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

Этот ответ ссылается на рецензируемую публикацию конференции с официальным издательским органом и официальной процедурой публикации и распространения.
mc00x1

0

Я создал родительский абстрактный общий класс построителя, который принимает два параметра формального типа. Первый - это тип объекта, возвращаемого функцией build (), второй - тип, возвращаемый каждым дополнительным установщиком параметров. Ниже приведены родительский и дочерний классы для наглядности:

// **Parent**
public abstract static class Builder<T, U extends Builder<T, U>> {
    // Required parameters
    private final String name;

    // Optional parameters
    private List<String> outputFields = null;


    public Builder(String pName) {
        name = pName;
    }

    public U outputFields(List<String> pOutFlds) {
        outputFields = new ArrayList<>(pOutFlds);
        return getThis();
    }


    /**
     * This helps avoid "unchecked warning", which would forces to cast to "T" in each of the optional
     * parameter setters..
     * @return
     */
    abstract U getThis();

    public abstract T build();



    /*
     * Getters
     */
    public String getName() {
        return name;
    }
}

 // **Child**
 public static class Builder extends AbstractRule.Builder<ContextAugmentingRule, ContextAugmentingRule.Builder> {
    // Required parameters
    private final Map<String, Object> nameValuePairsToAdd;

    // Optional parameters
    private String fooBar;


    Builder(String pName, Map<String, String> pNameValPairs) {
        super(pName);
        /**
         * Must do this, in case client code (I.e. JavaScript) is re-using
         * the passed in for multiple purposes. Doing {@link Collections#unmodifiableMap(Map)}
         * won't caught it, because the backing Map passed by client prior to wrapping in
         * unmodifiable Map can still be modified.
         */
        nameValuePairsToAdd = new HashMap<>(pNameValPairs);
    }

    public Builder fooBar(String pStr) {
        fooBar = pStr;
        return this;
    }


    @Override
    public ContextAugmentingRule build() {
        try {
            Rule r = new ContextAugmentingRule(this);
            storeInRuleByNameCache(r);
            return (ContextAugmentingRule) r;
        } catch (RuleException e) {
            throw new IllegalArgumentException(e);
        }
    }

    @Override
    Builder getThis() {
        return this;
    }
}

Этот удовлетворил мои потребности.

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.