如何改善构建模式?

时间:2009-10-28 17:14:22

标签: java design-patterns builder-pattern

动机

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

传统构建器模式

当我使用构建器模式创建我的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都返回一个不同的内部构建器类。每个内部构建器类只提供一个setter方法,最后一个只提供build()方法。

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

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

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

可选参数

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

问题

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

10 个答案:

答案 0 :(得分:23)

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

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

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

答案 1 :(得分:15)

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

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

答案 2 :(得分: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"));
    }
}

答案 3 :(得分: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;
     }
  }
...
}

此模式在Effective Java中列出。

答案 4 :(得分:7)

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

答案 5 :(得分:5)

恕我直言,这看起来很臃肿。如果 拥有所有参数,请在构造函数中传递它们。

答案 6 :(得分:5)

我见过/用过这个:

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

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

答案 7 :(得分:2)

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

  • 你的班级可能做得太多了。仔细检查它是否违反了Single Responsibility Principle。问问自己为什么需要一个包含这么多必需实例变量的类。
  • 您的构造函数可能是doing too much。构造函数的作用是构造。 (当他们命名时,他们没有变得非常有创意; D)就像课程一样,方法有单一责任原则。如果您的构造函数不仅仅是字段赋值,那么您需要有充分的理由来证明这一点。您可能会发现需要Factory Method而不是构建器。
  • 您的参数可能为doing too little。问问自己,您的参数是否可以分组为一个小结构(或Java的结构类对象)。不要害怕上小班。如果您确实发现需要创建结构或小类,请不要忘记属于结构而不是较大类的to refactor out functionality。< / LI>

答案 8 :(得分:1)

有关何时使用构建器模式及其优点的详细信息,请查看我的帖子,了解其他类似问题 here

答案 9 :(得分: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开始,然后如果你因为缺少方法调用而被咬伤,那就引入它。