【问题标题】:In java, can you use the builder pattern with required and reassignable fields?在 Java 中,您可以使用带有必填字段和可重新分配字段的构建器模式吗?
【发布时间】:2014-01-18 22:48:13
【问题描述】:

这与以下问题有关:

How to improve the builder pattern?

我很好奇是否可以实现具有以下属性的构建器:

  1. 部分或全部参数是必需的
  2. 没有方法接收很多参数(即,没有提供给初始构建器工厂方法的默认值列表)
  3. 所有构建器字段都可以重新分配任意次数
  4. 编译器应检查是否已设置所有参数
  5. 可以要求以某种顺序初始设置参数,但是一旦设置了任何参数,所有后续构建器都可以再次设置此参数(即,您可以重新分配您希望的构建器的任何字段的值)
  6. setter 不应存在重复代码(例如,构建器子类型中不应覆盖 setter 方法)

下面是一次失败的尝试(省略了空的私有构造函数)。考虑以下玩具构建器实现,并注意带有“Foo f2”的行有编译器错误,因为继承的 a 设置器返回 BuilderB,而不是 BuilderFinal。有没有办法使用java类型系统来参数化setter的返回类型来实现上述目标,或者以其他方式实现。

public final class Foo {

    public final int a;
    public final int b;
    public final int c;

    private Foo(
            int a,
            int b,
            int c) {
        this.a = a;
        this.b = b;
        this.c = c;
    }

    public static BuilderA newBuilder() {
        return new BuilderC();
    }

    public static class BuilderA {
        private volatile int a;

        public BuilderB a(int v) {
            a = v;
            return (BuilderB) this;
        }

        public int a() {
            return a;
        }
    }

    public static class BuilderB extends BuilderA {
        private volatile int b;

        public BuilderC b(int v) {
            b = v;
            return (BuilderC) this;
        }

        public int b() {
            return b;
        }
    }

    public static class BuilderC extends BuilderB {
        private volatile int c;

        public BuilderFinal c(int v) {
            c = v;
            return (BuilderFinal) this;
        }

        public int c() {
            return c;
        }
    }

    public static class BuilderFinal extends BuilderC {

        public Foo build() {
            return new Foo(
                    a(),
                    b(),
                    c());
        }
    }

    public static void main(String[] args) {
        Foo f1 = newBuilder().a(1).b(2).c(3).build();
        Foo f2 = newBuilder().a(1).b(2).c(3).a(4).build();
    }

}

【问题讨论】:

  • 如果BuilderC 返回一个BuilderAll 以便可以任意重新分配参数怎么办?顺便说一句,用户可以随意铸造您的构建器,从而破坏您的构建器的完整性。
  • 我不明白 BuilderAll 和 BuilderFinal 的区别。在客户端代码中抵抗强制转换只是另一个可取的要求,但由于我不知道如何在不要求对执行强制转换的客户端有弹性的情况下完成要求 1-6,所以我省略了它。
  • 顺便说一句,如果有解决方案,我希望它可能需要在内部使用强制转换,因为这是一个可变的构建器对象。我最初的想法是使用类型参数来存储 setter 的返回类型,以便 BuilderA 可以在调用它的 setter 时返回,例如,如果对象已经调用了 b 的 setter,则可以返回 BuilderC。但是,这种方法似乎失败了,因为类型参数是递归的。有没有这样的方式让子类继承方法并从这些方法返回自己类型的值而不覆盖?
  • 只是好奇,允许多次设置参数有什么意义?
  • @jonderry 我在更新代码和帖子时撤消了对帖子的删除,以展示生成器继承的通用版本,它返回正确的类型(而不是基本类型)

标签: java design-patterns


【解决方案1】:

您的要求确实很难,但看看这个通用解决方案是否符合要求:

public final class Foo {

    public final int a;
    public final int b;
    public final int c;

    private Foo(
            int a,
            int b,
            int c) {
        this.a = a;
        this.b = b;
        this.c = c;
    }

    public static BuilderA<? extends BuilderB<?>> newBuilder() {
        return new BuilderFinal();
    }

    public static class BuilderA<T extends BuilderB<?>> {
        private volatile int a;

        @SuppressWarnings("unchecked")
        public T a(int v) {
            a = v;
            return (T) this;
        }

        public int a() {
            return a;
        }
    }

    public static class BuilderB<T extends BuilderC<?>> extends BuilderA<T> {
        private volatile int b;

        @SuppressWarnings("unchecked")
        public T b(int v) {
            b = v;
            return (T) this;
        }

        public int b() {
            return b;
        }
    }

    public static class BuilderC<T extends BuilderFinal> extends BuilderB<T> {
        private volatile int c;

        @SuppressWarnings("unchecked")
        public T c(int v) {
            c = v;
            return (T) this;
        }

        public int c() {
            return c;
        }
    }

    public static class BuilderFinal extends BuilderC<BuilderFinal> {

        public Foo build() {
            return new Foo(
                    a(),
                    b(),
                    c());
        }
    }

    public static void main(String[] args) {
        Foo f1 = newBuilder().a(1).b(2).c(3).build();
        Foo f2 = newBuilder().a(1).b(2).c(3).a(4).build();
    }

}

【讨论】:

  • newBuilder 可以返回BuilderA&lt;?&gt;
  • 这就是解决方案。我正在做类似的事情,但你在我之前就做到了。这是有效的,因为所有方法都返回“捕获”,直到您到达 BuilderFinal。然后所有的方法都返回一个 BuilderFinal。
  • 非常好!这是我最初试图得到的,但我不知道如何使用这样的通配符来避免类型参数递归问题。
  • 其实这里面有个bug。考虑以下内容:“Foo f = newBuilder().a(4).a(5).c(34).build()”。
  • 对此问题有评论。原因是您可能希望使用同一个构建器构建多个对象,例如,如果它是一个配置构建器,并且您希望生成一系列快照并确保一些最小配置集设置为至少一些值。将这些值传递给构建器工厂方法是一种选择,但是您可以在代码的不同部分中提取配置,或者可能有太多以至于您想要构建器用于构建器的工厂方法的 arg 列表。您可以在运行时检查它们,但最好让编译器捕获它。
【解决方案2】:

据我所知,应该使用构建器模式,以防使用多个参数,这会使调用变得相当复杂,因为参数可能会交换位置或无法明确明确哪个参数的用途。

经验法则是在构建器的构造函数中要求强制参数,在方法中要求可选参数。然而,通常可能需要超过 4 个参数,这使得调用再次变得相当不清楚并且模式冗余。因此,也可以使用拆分为默认构造函数和每个参数的参数设置。

检查应该在构建方法中调用的自己的方法中进行,因此您可以使用super 调用它。编译时安全性仅在正确的数据类型上得到保证(只有异常 - null 是可能的,这必须在checkParameters()-方法中获取)。但是,您可以强制在 Builder 构造函数中将所有必需参数设置为需要它们 - 但如前所述,这可能会导致冗余模式。

import java.util.ArrayList;
import java.util.List;

public class C
{
    public static class Builder<T extends C, B extends C.Builder<? extends C,? extends B>> extends AbstractBuilder<C>
    {
        protected String comp1;
        protected String comp2;
        protected int comp3;
        protected int comp4;
        protected int comp5;
        protected List<Object> comp6 = new ArrayList<>();
        protected String optional1;
        protected List<Object> optional2 = new ArrayList<>();

        public Builder()
        {

        }

        public B withComp1(String comp1)
        {
            this.comp1 = comp1;
            return (B)this;
        }

        public B withComp2(String comp2)
        {
            this.comp2 = comp2;
            return (B)this;
        }

        public B withComp3(int comp3)
        {
            this.comp3 = comp3;
            return (B)this;
        }

        public B withComp4(int comp4)
        {
            this.comp4 = comp4;
            return (B)this;
        }

        public B withComp5(int comp5)
        {
            this.comp5 = comp5;
            return (B)this;
        }

        public B withComp6(Object comp6)
        {
            this.comp6.add(comp6);
            return (B)this;
        }

        public B withOptional1(String optional1)
        {
            this.optional1 = optional1;
            return (B)this;
        }

        public B withOptional2(Object optional2)
        {
            this.optional2.add(optional2);
            return (B)this;
        }

        @Override
        protected void checkParameters() throws BuildException
        {
            if (this.comp1 == null)
                throw new BuildException("Comp1 violates the rules");
            if (this.comp2 == null)
                throw new BuildException("Comp2 violates the rules");
            if (this.comp3 == 0)
                throw new BuildException("Comp3 violates the rules");
            if (this.comp4 == 0)
                throw new BuildException("Comp4 violates the rules");
            if (this.comp5 == 0)
                throw new BuildException("Comp5 violates the rules");
            if (this.comp6 == null)
                throw new BuildException("Comp6 violates the rules");
        }

        @Override
        public T build() throws BuildException
        {
            this.checkParameters();

            C c = new C(this.comp1, this.comp2,this.comp3, this.comp4, this.comp5, this.comp6);
            c.setOptional1(this.optional1);
            c.setOptional2(this.optional2);
            return (T)c;
        }
    }

    private final String comp1;
    private final String comp2;
    private final int comp3;
    private final int comp4;
    private final int comp5;
    private final List<?> comp6;
    private String optional1;
    private List<?> optional2;

    protected C(String comp1, String comp2, int comp3, int comp4, int comp5, List<?> comp6)
    {
        this.comp1 = comp1;
        this.comp2 = comp2;
        this.comp3 = comp3;
        this.comp4 = comp4;
        this.comp5 = comp5;
        this.comp6 = comp6;
    }

    public void setOptional1(String optional1)
    {
        this.optional1 = optional1;
    }

    public void setOptional2(List<?> optional2)
    {
        this.optional2 = optional2;
    }

    // further methods omitted

    @Override
    public String toString()
    {
        StringBuilder sb = new StringBuilder();
        sb.append(this.comp1);
        sb.append(", ");
        sb.append(this.comp2);
        sb.append(", ");
        sb.append(this.comp3);
        sb.append(", ");
        sb.append(this.comp4);
        sb.append(", ");
        sb.append(this.comp5);
        sb.append(", ");
        sb.append(this.comp6);

        return sb.toString();
    }
}

在从 C 和构建器扩展 D 时,您需要覆盖 checkParameters()build() 方法。由于使用了泛型,调用build()时将返回正确的类型@

import java.util.List;

public class D extends C
{
    public static class Builder<T extends D, B extends D.Builder<? extends D, ? extends B>> extends C.Builder<D, Builder<D, B>>
    {
        protected String comp7;

        public Builder()
        {

        }

        public B withComp7(String comp7)
        {
            this.comp7 = comp7;
            return (B)this;
        }

        @Override
        public void checkParameters() throws BuildException
        {
            super.checkParameters();

            if (comp7 == null)
                throw new BuildException("Comp7 violates the rules");
        }

        @Override
        public T build() throws BuildException
        {
            this.checkParameters();

            D d = new D(this.comp1, this.comp2, this.comp3, this.comp4, this.comp5, this.comp6, this.comp7);

            if (this.optional1 != null)
                d.setOptional1(optional1);
            if (this.optional2 != null)
                d.setOptional2(optional2);

            return (T)d;
        }
    }

    protected String comp7;

    protected D(String comp1, String comp2, int comp3, int comp4, int comp5, List<?> comp6, String comp7)
    {
        super(comp1, comp2, comp3, comp4, comp5, comp6);
        this.comp7 = comp7;
    }

    @Override
    public String toString()
    {
        StringBuilder sb = new StringBuilder();
        sb.append(super.toString());
        sb.append(", ");
        sb.append(this.comp7);
        return sb.toString();
    }
}

抽象构建器类非常简单:

public abstract class AbstractBuilder<T>
{
    protected abstract void checkParameters() throws BuildException;

    public abstract <T> T build() throws BuildException;
}

例外也很简单:

public class BuildException extends Exception
{
    public BuildException(String msg)
    {
        super(msg);
    }
}

最后但并非最不重要的主要方法:

public static void main(String ... args)
{
    try
    {
        C c = new C.Builder<>().withComp1("a1").withComp2("a2").withComp3(1)
            .withComp4(4).withComp5(5).withComp6("lala").build();
        System.out.println("c: " + c);

        D d = new D.Builder<>().withComp1("d1").withComp2("d2").withComp3(3)
            .withComp4(4).withComp5(5).withComp6("lala").withComp7("d7").build();
        System.out.println("d: " + d);

        C c2 = new C.Builder<>().withComp1("a1").withComp3(1)
            .withComp4(4).withComp5(5).withComp6("lala").build();
        System.out.println(c2);
    }
    catch (Exception e)
    {
        e.printStackTrace();
    }
}

输出:

c: a1, a2, 1, 4, 5, [lala]
d: d1, d2, 3, 4, 5, [lala], d7
Builders.BuildException: Comp2 violates the rules
        ... // StackTrace omitted

不过,在对泛型进行过多处理之前,我建议坚持 KISS 政策并忘记构建器的继承,并将它们编码为简单而愚蠢的代码(其中一部分包括愚蠢的复制和粘贴)


@edit:好的,在完成所有工作并重新阅读 OP 以及链接的帖子之后,我对要求的假设完全错误 - 就像德语的措辞说:“手术成功,病人死了” -虽然我把这篇文章留在这里,以防有人想要一个类似复制和粘贴的解决方案,它实际上返回正确的类型而不是基本类型

【讨论】:

  • 具有许多参数的构造函数违反了要求 2(我假设初始构造函数是通过具有零个或一个参数的工厂方法构造的)。需求5是我的代码的问题,满足1、2、3、4、6。
  • 这就是我包含默认构造函数的原因。你能用一些例子解释5)吗?要么为时已晚,要么我只是不明白(或两者兼而有之:P)
  • 实现 1-4 和 6 的一种方法是使用一系列类型,每个类型都保证调用一个额外的 setter,因为构造每种类型的实例的唯一方法是调用“上一个”类型的设置器。但是,如果您返回并调用较早的 setter,编译器将不再“记住”先前的值已被设置,因为对象的编译时类型会恢复为较早的版本。尝试粘贴我的代码,看看 f1 和 f2 会发生什么。
【解决方案3】:

我有一次a crazy idea,它有点违背你的一些要求,但我认为你可以让构建器构造函数获取所需的参数,但是以使其仍然存在的方式清除正在设置的参数。看看:

package myapp;

public final class Foo {

  public final int a;
  public final int b;
  public final int c;

  private Foo(int a, int b, int c) {
      this.a = a;
      this.b = b;
      this.c = c;
  }

  public static class Builder {
    private int a;
    private int b;
    private int c;

    public Builder(A a, B b, C c) {
      this.a = a.v;
      this.b = b.v;
      this.c = c.v;
    }

    public Builder a(int v) { a = v; return this; }
    public Builder b(int v) { b = v; return this; }
    public Builder c(int v) { c = v; return this; }

    public Foo build() {
      return new Foo(a, b, c);
    }
  }

  private static class V {
    int v;
    V(int v) { this.v = v; }
  }
  public static class A extends V { A(int v) { super(v); } }
  public static class B extends V { B(int v) { super(v); } }
  public static class C extends V { C(int v) { super(v); } }
  public static A a(int v) { return new A(v); }
  public static B b(int v) { return new B(v); }
  public static C c(int v) { return new C(v); }

  public static void main(String[] args) {
    Foo f1 = new Builder(a(1), b(2), c(3)).build();
    Foo f2 = new Builder(a(1), b(2), c(3)).a(4).build();
  }

}

对于其他客户端,静态导入是您的朋友:

package myotherapp;

import myapp.Foo;
import static myapp.Foo.*;

public class Program {

  public static void main(String[] args) {
    Foo f1 = new Builder(a(1), b(2), c(3)).build();
    Foo f2 = new Builder(a(1), b(2), c(3)).a(4).build();
  }

}

【讨论】:

    【解决方案4】:

    基于 Jordão 的想法,我想出了以下内容,即使类型参数中有一些重复的代码,它也可以满足 1-6 的所有要求。本质上,这个想法是通过使用类型参数来“传递”每个方法的返回类型来覆盖继承方法的返回值。尽管代码冗长且不切实际,并且如果您将其扩展到任意数量的字段 n,实际上需要 Omega(n^3) 个字符,但我发布它是因为我认为它是 java 类型系统的一个有趣用途。如果有人能找到减少类型参数数量的方法(尤其是渐近),请在 cmets 中发帖或写另一个答案。

    public final class Foo {
    
        public final int a;
        public final int b;
        public final int c;
    
        private Foo(
                int a,
                int b,
                int c) {
            this.a = a;
            this.b = b;
            this.c = c;
        }
    
        public static BuilderA<? extends BuilderB<?, ?>, ? extends BuilderC<?, ?>> newBuilder() {
            return new BuilderFinal();
        }
    
        public static class BuilderA<B extends BuilderB<?, ?>, C extends BuilderC<?, ?>> {
            private volatile int a;
    
            @SuppressWarnings("unchecked")
            public B a(int v) {
                a = v;
                return (B) this;
            }
    
            public int a() {
                return a;
            }
        }
    
        public static class BuilderB<B extends BuilderB<?, ?>, C extends BuilderC<?, ?>> extends BuilderA<B, C> {
            private volatile int b;
    
            @SuppressWarnings("unchecked")
            public C b(int v) {
                b = v;
                return (C) this;
            }
    
            public int b() {
                return b;
            }
        }
    
        public static class BuilderC<B extends BuilderC<?, ?>, C extends BuilderC<?, ?>> extends BuilderB<B, C> {
            private volatile int c;
    
            @SuppressWarnings("unchecked")
            public BuilderFinal c(int v) {
                c = v;
                return (BuilderFinal) this;
            }
    
            public int c() {
                return c;
            }
        }
    
        public static class BuilderFinal extends BuilderC<BuilderFinal, BuilderFinal> {
    
            public Foo build() {
                return new Foo(
                        a(),
                        b(),
                        c());
            }
        }
    
        public static void main(String[] args) {
            Foo f1 = newBuilder().a(1).b(2).c(3).a(2).build();
            Foo f2 = newBuilder().a(1).a(2).c(3).build(); // compile error
            Foo f3 = newBuilder().a(1).b(2).a(3).b(4).b(5).build(); // compile error
        }
    
    }
    

    【讨论】:

      【解决方案5】:

      您为什么不想覆盖 BuilderFinal 中的设置器?他们只需要向下转换 super 方法:

      public static class BuilderFinal extends BuilderC {
      
          @Override
          public BuilderFinal a(int v) {
              return (BuilderFinal) super.a(v);
          }
      
          @Override
          public BuilderFinal b(int v) {
              return (BuilderFinal) super.b(v);
          }
      
          public Foo build() {
              return new Foo(
                      a(),
                      b(),
                      c());
          }
      }
      

      【讨论】:

      • 这在问题中是明确禁止的。
      • 是的,我知道这就是我问的原因。在我的书中它不算重复代码。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2015-06-04
      • 1970-01-01
      • 2021-07-23
      • 2017-07-03
      • 2021-12-18
      • 2014-03-04
      • 2017-10-01
      相关资源
      最近更新 更多