45

动机

最近我在寻找一种方法来初始化一个复杂的对象,而不需要向构造函数传递很多参数。我尝试使用构建器模式,但我不喜欢这样一个事实,即我无法在编译时检查是否真的设置了所有需要的值。

传统建造者模式

当我使用构建器模式创建Complex对象时,创建过程更加“类型安全”,因为更容易看到参数的用途:

new ComplexBuilder()
        .setFirst( "first" )
        .setSecond( "second" )
        .setThird( "third" )
        ...
        .build();

但是现在我有一个问题,我很容易错过一个重要的参数。我可以在方法中检查它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;
            }
        }
    }
}

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

现在对象的构造再次看起来像这样:

new ComplexBuilder()
    .setFirst( "first" )
    .setSecond( "second" )
    .setThird( "third" )
    .build();

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

可选参数

如果我有可选参数,我会使用最后一个内部构建器类Builder4来设置它们,就像“传统”构建器一样,返回自身。

问题

  • 这是众所周知的模式吗?它有一个特殊的名字吗?
  • 你看到任何陷阱吗?
  • 您是否有任何改进实施的想法 - 在更少的代码行的意义上?
4

10 回答 10

24

传统的构建器模式已经解决了这个问题:只需在构造器中获取强制参数。当然,没有什么能阻止调用者传递 null,但您的方法也不会。

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

此外,这是很多额外的工作。

于 2009-10-28T17:23:31.313 回答
17

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

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

于 2009-10-28T17:20:28.857 回答
16
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"));
    }
}
于 2013-07-27T09:10:11.653 回答
13

为什么不在构建器构造函数中放置“需要的”参数?

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

此模式在Effective Java中进行了概述。

于 2009-10-28T17:17:47.363 回答
7

我不会使用多个类,而是只使用一个类和多个接口。它强制执行您的语法,而不需要太多的输入。它还使您可以近距离查看所有相关代码,从而更容易在更大的层面上理解您的代码正在发生的事情。

于 2011-07-07T15:48:13.800 回答
5

我见过/使用过这个:

new ComplexBuilder(requiredvarA, requiedVarB).optional(foo).optional(bar).build();

然后将它们传递给需要它们的对象。

于 2009-10-28T17:24:31.467 回答
5

恕我直言,这似乎臃肿。如果您必须拥有所有参数,请将它们传递到构造函数中。

于 2009-10-28T17:21:52.860 回答
2

当您有很多可选参数时,通常使用构建器模式。如果您发现需要许多必需参数,请首先考虑以下选项:

  • 你的班级可能做得太多了。仔细检查它是否违反单一责任原则。问问自己为什么需要一个包含这么多必需实例变量的类。
  • 你的构造函数可能做的太多了。构造函数的工作是构造。(当他们命名它时,他们并没有很有创意;D)就像类一样,方法也有一个单一的责任原则。如果您的构造函数所做的不仅仅是字段分配,那么您需要一个充分的理由来证明这一点。您可能会发现您需要工厂方法而不是生成器。
  • 您的参数可能做得太少。问问自己你的参数是否可以组合成一个小的结构(或者在 Java 的情况下是类似结构的对象)。不要害怕做小班。如果您确实发现需要创建结构或小类,请不要忘记重构属于 结构而不是大类的功能
于 2013-02-23T18:55:11.173 回答
1

有关何时使用构建器模式及其优势的更多信息,您应该在此处查看我的帖子以了解另一个类似问题

于 2009-12-23T18:42:09.580 回答
0

问题1:关于模式的名称,我喜欢“Step Builder”这个名称:

问题 2/3:关于陷阱和建议,在大多数情况下感觉过于复杂。

  • 您正在强制执行如何使用构建器的顺序,这在我的经验中是不寻常的。我可以看到这在某些情况下是多么重要,但我从来不需要它。例如,我认为不需要在此处强制执行序列:

    Person.builder().firstName("John").lastName("Doe").build() Person.builder().lastName("Doe").firstName("John").build()

  • 但是,很多时候构建器需要强制执行一些约束来防止构建虚假对象。也许您想确保提供了所有必填字段或字段组合有效。我猜这是您想要在建筑物中引入排序的真正原因。

    在这种情况下,我喜欢 Joshua Bloch 的建议,在 build() 方法中进行验证。这有助于跨字段验证,因为此时一切都可用。看到这个答案:https ://softwareengineering.stackexchange.com/a/241320

总之,我不会因为您担心“丢失”对构建器方法的调用而向代码添加任何复杂性。在实践中,这很容易被测试用例捕捉到。也许从一个 vanilla Builder 开始,如果你一直被缺少的方法调用所困扰,然后再引入它。

于 2015-03-05T20:30:57.397 回答