Java构建器类的子类化

160

这篇Dr Dobbs文章中,特别是建造者模式,当我们需要对一个Builder进行子类化时,该如何处理?以我们想要添加GMO标签的示例为例,一个天真的实现方式是:

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 返回的是一个 NutritionFacts.Builder,而不是一个 GMOFacts.Builder,那么我们如何解决这个问题呢?还是有更好的模式可以使用吗?
注意:这里是对类似问题的答案 提供了上面的类;我的问题是如何确保建造者调用的顺序正确。

1
我认为以下链接描述了一个很好的方法:http://egalluzzo.blogspot.co.at/2010/06/using-inheritance-with-fluent.html - stuXnet
2
但是你如何构建 b.GMO(true).calories(100) 的输出呢? - Sridhar Sarnobat
10个回答

205
你可以使用泛型解决这个问题。我认为这被称为“奇妙的递归泛型模式”
将基类构造器方法的返回类型作为泛型参数。
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
哎呀,我想我要么(a)发布一个新问题,(b)重新设计使用“implements”而不是“extends”,或者(c)放弃一切。现在我有一个奇怪的编译错误,其中leafBuilder.leaf().leaf()leafBuilder.mid().leaf()是可以的,但是leafBuilder.leaf().mid().leaf()失败了... - Ken Y-N
12
@gkamal 的 return (T) this; 会触发 unchecked or unsafe operations 警告。这是不可避免的,对吗? - Dmitry Minkovsky
5
为了解决"unchecked cast"警告,请参考下面其他回答中建议的解决方案:https://dev59.com/J2Qn5IYBdhLWcg3wAjTJ#34741836 - Stepan Vavra
11
请注意,Builder<T extends Builder> 实际上是一个 裸类型(rawtype) - 应该改为 Builder<T extends Builder<T>> - Boris the Spider
3
@user2957378,“GMOFacts”的“Builder”也需要成为通用的“Builder<B extends Builder<B>> extends NutritionFacts.Builder<Builder>”,并且这个模式可以一级级地延续下去。如果声明一个非通用的builder,则无法扩展该模式。 - Boris the Spider
显示剩余5条评论

52

仅供记录,为了消除

unchecked or unsafe operations 警告

针对@dimadima和@Thomas N.所讨论的 return (T) this; 语句,以下解决方案适用于某些情况。

使构建器(builder)抽象化,声明泛型类型(在这种情况下是 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()方法返回的是NutritionFacts? - mvd
@mvd 因为这是对问题的答案?在子类型中,您将重写它,例如 public GMOFacts build() { return new GMOFacts(this); } - Stepan Vavra
当我们想要添加第二个子类BuilderC extends BuilderBBuilderB extends BuilderA时,问题出现在BuilderB不是抽象的情况下。 - wrozwad
2
这不是问题的答案,因为基类可能不是抽象的! - Roland
将声明通用类型的构建器抽象化 - 如果我想直接使用该构建器怎么办? - K--

22

根据一篇博客文章,这种方法要求所有非叶类必须是抽象的,而所有的叶子类必须是final。

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

你知道为什么叶子类需要是final的吗?我希望我的具体类可以被子类化,但是我还没有找到一种让编译器理解B类型的方法,它总是变成基类。 - David Ganster
1
请注意,LeafClass中的Builder类不遵循中间层SecondLevel类所使用的<T extends SomeClass, B extends SomeClass.Builder<T,B>> extends SomeClassParent.Builder<T,B>模式,而是声明了特定类型。只有当使用特定类型到达叶子节点时才能实例化类,但是一旦这样做,就无法进一步扩展该类,因为您正在使用特定类型并且已经放弃了奇怪的递归模板模式。这个链接可能会有所帮助:http://www.angelikalanger.com/GenericsFAQ/FAQSections/TypeParameters.html#FAQ106 - 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++背景。对于这个小例子来说,这是一个有用的方法,但是对于一个完整的类来说,重复所有的方法会变得很痛苦,而且容易出错。感谢你教给我新东西! - Ken Y-N
在我看来,这似乎解决不了任何问题。子类化父类的原因(依我之见)是为了重用父类的方法而不覆盖它们。如果这些类只是简单的值对象,在构建器方法中没有真正的逻辑,除了设置一个简单的值,那么在覆盖方法中调用父类方法几乎没有价值。 - Developer Dude
答案解决了问题描述中的问题:使用构建器的代码可以编译两种排序。既然一种方式可以编译而另一种方式不能,我猜肯定有某些价值。 - Flavio

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

3

还有一种按照“组合优于继承”原则创建类的方法。

定义一个接口,父类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.BuilderGMOFacts.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;
    }
}

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()

这个静态工厂方法会返回适当的生成器。您可以有一个扩展NutritionFacts.BuilderGMOFacts.Builder,这不是问题。问题在于如何处理可见性问题...

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

这个已经完全满足了我的需求。


-2
下面的 IEEE 投稿 Java 中改良的流畅 Builder 给出了该问题的全面解决方案。它将原问题分解成两个子问题:继承缺陷和准不变性,并展示了如何通过解决这两个子问题,在 Java 的经典 Builder 模式中实现继承支持和代码重用。

4
这个回答没有任何有用的信息,也没有至少包含给定链接中的答案摘要,而且链接需要登录。 - Sonata
这个答案链接到一个经过同行评审的会议出版物,具有官方出版机构和官方出版和共享程序。 - mc00x1

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接