在java中,您可以将构建器模式与必需和可重新分配的字段一起使用吗?

时间:2014-01-18 22:48:14

标签: java design-patterns

这与以下问题有关:

How to improve the builder pattern?

我很想知道是否可以实现具有以下属性的构建器:

  1. 部分或全部参数是必需的
  2. 没有方法接收许多参数(即,没有提供给初始构建器工厂方法的默认值列表)
  3. 可以重新分配所有构建器字段
  4. 编译器应检查是否已设置所有参数
  5. 可以要求参数最初按某种顺序设置,但是一旦设置了任何参数,所有后续构建器都可以再次设置此参数(即,您可以重新分配您希望的构建器的任何字段的值)
  6. 设置者不应存在重复的代码(例如,构建器子类型中没有重写的setter方法)
  7. 下面是一个失败的尝试(省略了空的私有构造函数)。考虑以下玩具构建器实现,并注意与" Foo f2"有一个编译器错误,因为a的继承setter返回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();
        }
    
    }
    

5 个答案:

答案 0 :(得分:3)

您的要求非常难,但请查看此通用解决方案是否符合要求:

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

}

答案 1 :(得分:2)

据我所知,应该使用构建器模式,以防使用多个参数使调用变得相当复杂,因为参数可能会交换位置或者不清楚明确哪个参数用于什么。

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

检查应该在自己的方法中进行,该方法在build-method中调用,因此您可以使用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

尽管在使用Generics之前,我建议坚持使用KISS策略并忘记构建器的继承并将它们编码为简单和愚蠢(其中一部分包括哑副本和粘贴)


@edit:好的,在完成所有工作并重新阅读OP以及链接的帖子后,我对这些要求做了完全错误的假设 - 就像德国的措辞所说:“手术成功,病人已经死了” - 虽然我在这里留下这篇文章以防万一有人想要一个副本并且粘贴像解决方案一样的构建器继承实际上返回正确的类型而不是基类型

答案 2 :(得分:1)

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

}

答案 3 :(得分:0)

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

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
    }

}

答案 4 :(得分:-1)

为什么不想覆盖BuilderFinal中的setter?他们只需要转向超级方法:

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