Підкласифікація класу Java Builder


133

Надайте цю статтю доктора Доббса , а саме Шаблон будівельника, як ми маємо справу з випадком підкласифікації 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()не вдається ...
Кен ЮН

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

5
Щоб вирішити unchecked castпопередження, див. Рішення, запропоноване нижче, серед інших відповідей: stackoverflow.com/a/34741836/3114959
Степан Вавра

8
Зауважте, що Builder<T extends Builder>насправді є сировиною - таким має бути Builder<T extends Builder<T>>.
Борис Павук

2
@ user2957378 the Builderfor GMOFactsтакож повинні бути загальними Builder<B extends Builder<B>> extends NutritionFacts.Builder<Builder>- і ця модель може продовжуватись знижувати стільки рівнів, скільки потрібно. Якщо ви заявляєте, що не є загальним конструктором, ви не можете продовжити шаблон.
Борис Павук

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
Sosite

1
Це не відповідь на питання, оскільки базовий клас може бути не абстрактним!
Roland

"Зробити абстрактним будівельник, який оголошує загальний тип" - що робити, якщо я хотів би скористатися цим будівельником безпосередньо?
ромашка

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 клас, він оголошує конкретні типи. Ви не можете встановити клас, поки не потрапите до листа, використовуючи конкретні типи, але як тільки ви це зробите, ви більше не можете його продовжувати, оскільки ви використовуєте конкретні типи та відмовилися від цікаво повторюваного шаблону шаблонів. Це посилання може допомогти: 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 за те, що я навчив мене чомусь новому!
Кен ІН

Мені здалося б, що це нічого не вирішує. Причиною (ІМО) для підкласингу батьків є повторне використання батьківських методів, не змінюючи їх. Якщо класи - це просто об'єкти значення, що не мають реальної логіки в методах builder, за винятком встановлення простого значення, то виклик батьківського методу в методі переосмислення має мало значення.
Чувак розробника

Відповідь вирішує задачу, описану в питанні: код за допомогою конструктора компілюється з обома замовленнями. Оскільки один спосіб компілює, а інший - ні, я думаю, має бути якесь значення.
Флавіо

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

Приклад повного 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 + '\'' +
        '}';
  }
}

Ключові моменти:

  • Інкапсулюйте об'єкт у програмі-конструкторі так, щоб успадкування заважало встановлювати поле на об'єкті, що міститься у батьківському типі
  • Виклики супер гарантувати, що логіка (якщо така є), додана до методів конструктора супер типів, зберігається в підтипах.
  • Внизу стоїть помилкове створення об'єктів у батьківському класі (класах) ... Але див. Спосіб, як очистити це
  • Верхню сторону набагато простіше зрозуміти з першого погляду, і ніяких багатослівних конструкторів, що передають властивості.
  • Якщо у вас є кілька потоків, що звертаються до об’єктів вашого будівельника ... я думаю, я радий, що я не ви :).

Редагувати:

Я знайшов шлях навколо створення хибного об'єкта. Спочатку додайте це до кожного будівельника:

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.