如何改善构建模式?

tan*_*ens 39 java design-patterns builder-pattern

动机

最近我搜索了一种初始化复杂对象的方法,而没有将大量参数传递给构造函数.我尝试使用构建器模式,但我不喜欢这样的事实,即如果我确实设置了所有需要的值,我无法在编译时检查.

传统建设者模式

当我使用构建器模式创建我的Complex对象时,创建更"类型安全",因为它更容易看到用于什么参数:

new ComplexBuilder()
        .setFirst( "first" )
        .setSecond( "second" )
        .setThird( "third" )
        ...
        .build();
Run Code Online (Sandbox Code Playgroud)

但现在我遇到了问题,我很容易错过一个重要的参数.我可以在build()方法中检查它,但这只是在运行时.如果我错过了什么,在编译时没有什么可以警告我.

增强的构建器模式

现在我的想法是创建一个构建器,"提醒"我是否错过了所需的参数.我的第一次尝试看起来像这样:

public class Complex {
    private String m_first;
    private String m_second;
    private String m_third;

    private Complex() {}

    public static class ComplexBuilder {
        private Complex m_complex;

        public ComplexBuilder() {
            m_complex = new Complex();
        }

        public Builder2 setFirst( String first ) {
            m_complex.m_first = first;
            return new Builder2();
        }

        public class Builder2 {
            private Builder2() {}
            Builder3 setSecond( String second ) {
                m_complex.m_second = second;
                return new Builder3();
            }
        }

        public class Builder3 {
            private Builder3() {}
            Builder4 setThird( String third ) {
                m_complex.m_third = third;
                return new Builder4();
            }
        }

        public class Builder4 {
            private Builder4() {}
            Complex build() {
                return m_complex;
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

如您所见,构建器类的每个setter返回不同的内部构建器类.每个内部构建器类只提供一个setter方法,最后一个只提供build()方法.

现在,对象的构造再次如下所示:

new ComplexBuilder()
    .setFirst( "first" )
    .setSecond( "second" )
    .setThird( "third" )
    .build();
Run Code Online (Sandbox Code Playgroud)

...但是没有办法忘记所需的参数.编译器不会接受它.

可选参数

如果我有可选参数,我会使用最后一个内部构建器类Builder4来设置它们像"传统"构建器那样,返回自己.

问题

  • 这是众所周知的模式吗?它有一个特殊的名字吗?
  • 你看到任何陷阱吗?
  • 您是否有任何改进实施的想法 - 从代码行数较少的角度来看?

Mic*_*rdt 23

传统的构建器模式已经处理了这个问题:只需在构造函数中获取必需参数即可.当然,没有什么能阻止调用者传递null,但是你的方法也没有.

我用你的方法看到的一个大问题是你要么有一个强制参数数量的类的组合爆炸,要么强迫用户在一个特定的sqeuence中设置参数,这很烦人.

此外,还有很多额外的工作.

  • 海报特别提到试图避免将大量参数传递给构造函数,大概是因为这不会给你带来非常好的编译时间检查 - 因为如果你传递5个整数,编译器就不能告诉你你是否'我们以正确的顺序得到它们. (7认同)
  • 但是地球上没有编译器可以告诉我我应该键入`foo(5,6)`而不是`foo(6,5)`.问题中提出的流畅界面使得(略微?)不太可能,但并不能通过远距离消除这种可能性. (2认同)
  • "强制用户以一个特定的顺序设置参数" - 非常好的一点.我认为"流畅的生成器"或"布洛赫生成器"的优点之一是能够以您喜欢的顺序设置参数,并且失去它会使您向后退一步,只需一个伸缩构造器. (2认同)

Esk*_*sko 15

不,这不是新的.您实际在做的是通过扩展标准构建器模式来支持分支来创建一种DSL,这是确保构建器不会对实际对象产生一组冲突设置的绝佳方法.

我个人认为这是对构建器模式的一个很好的扩展,你可以用它做各种有趣的事情,例如在工作中我们有一些DSL构建器用于我们的一些数据完整性测试,这些测试允许我们做类似的事情assertMachine().usesElectricity().and().makesGrindingNoises().whenTurnedOn();.好吧,也许不是最好的例子,但我认为你明白了.


cli*_*nux 13

public class Complex {
    private final String first;
    private final String second;
    private final String third;

    public static class False {}
    public static class True {}

    public static class Builder<Has1,Has2,Has3> {
        private String first;
        private String second;
        private String third;

        private Builder() {}

        public static Builder<False,False,False> create() {
            return new Builder<>();
        }

        public Builder<True,Has2,Has3> setFirst(String first) {
            this.first = first;
            return (Builder<True,Has2,Has3>)this;
        }

        public Builder<Has1,True,Has3> setSecond(String second) {
            this.second = second;
            return (Builder<Has1,True,Has3>)this;
        }

        public Builder<Has1,Has2,True> setThird(String third) {
            this.third = third;
            return (Builder<Has1,Has2,True>)this;
        }
    }

    public Complex(Builder<True,True,True> builder) {
        first = builder.first;
        second = builder.second;
        third = builder.third;
    }

    public static void test() {
        // Compile Error!
        Complex c1 = new Complex(Complex.Builder.create().setFirst("1").setSecond("2"));

        // Compile Error!
        Complex c2 = new Complex(Complex.Builder.create().setFirst("1").setThird("3"));

        // Works!, all params supplied.
        Complex c3 = new Complex(Complex.Builder.create().setFirst("1").setSecond("2").setThird("3"));
    }
}
Run Code Online (Sandbox Code Playgroud)

  • 好主意啊!这样您就不会受限于设置参数的特定顺序,但您仍然可以确保已设置所有参数.谢谢你这个鼓舞人心的想法! (2认同)

Mar*_*tin 12

为什么不在构建器构造函数中放入"required"参数?

public class Complex
{
....
  public static class ComplexBuilder
  {
     // Required parameters
     private final int required;

     // Optional parameters
     private int optional = 0;

     public ComplexBuilder( int required )
     {
        this.required = required;
     } 

     public Builder setOptional(int optional)
     {
        this.optional = optional;
     }
  }
...
}
Run Code Online (Sandbox Code Playgroud)

Effective Java中概述了此模式.

  • 因为显然他们都需要并且有很多人.虽然如果是这样的话,我想知道其他地方是否还有改进的余地. (9认同)

Sle*_*led 7

我只使用一个类和多个接口,而不是使用多个类.它强制执行您的语法,而无需输入太多内容.它还允许您查看所有相关代码,这样可以更容易地了解代码在更大级别上发生的情况.


JRL*_*JRL 5

恕我直言,这似乎臃肿.如果必须包含所有参数,请在构造函数中传递它们.

  • Builder模式的要点是将它们放在构造函数中是有问题的(跟踪顺序等). (7认同)
  • 构建器模式不是将排序与特定步骤分开,以便"构建器"知道如何执行单个步骤,而"导向器"类对它们进行排序吗?也就是说这是模板模式的一个特例. (2认同)

rec*_*les 5

我见过/用过这个:

new ComplexBuilder(requiredvarA, requiedVarB).optional(foo).optional(bar).build();
Run Code Online (Sandbox Code Playgroud)

然后将这些传递给需要它们的对象.