【问题标题】:How to improve the builder pattern?如何改进建造者模式?
【发布时间】:2010-12-10 22:58:10
【问题描述】:

动机

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

传统的建造者模式

当我使用构建器模式创建 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 来设置它们,就像“传统”构建器一样,返回自身。

问题

  • 这是众所周知的模式吗?它有什么特别的名字吗?
  • 您发现任何陷阱吗?
  • 您是否有任何想法来改进实施 - 就减少代码行而言?

【问题讨论】:

  • 您的“传统构建器模式”看起来更像我所知道的 [流利界面][1]。当我听到“建造者模式”时,我想到了[设计模式“建造者”][2]。将流畅的界面称为构建器模式是否很常见,还是我在这里遗漏了什么? [1]:en.wikipedia.org/wiki/Fluent_interface [2]:en.wikipedia.org/wiki/Builder_pattern
  • @Ewan Joshua 博客将此模式称为“构建器”(但不能替代 GoF 构建器)。见rwhansen.blogspot.com/2007/07/…。所以,不知道用同名好不好,不知道是不是通用,但是作为Effective Java的读者,不觉得累赘。
  • 听起来您正在尝试使用构造函数进行编译时间检查。它的同一个 Java 不支持命名参数,如 groovy 等,无需额外的构建器类即可解决此问题。
  • 使用泛型实际上可以做得更好:michid.wordpress.com/2008/08/13/…
  • 这是一个聪明的想法,但是您失去了使用传统构建器模式获得的很多灵活性。不仅在您指定属性的顺序方面,还包括在变量中引用正在进行的构建器和异步设置构建器属性之类的事情。您不能对您的模式执行此操作,因为每次设置属性时构建器的类型都会更改。实际上,您所做的只是创建了一个更详细的构造函数,您不必查找每个参数的含义,而必须查找方法的正确顺序。您的 IDE 可以为您提供任一方面的帮助。

标签: java design-patterns builder-pattern


【解决方案1】:

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

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

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

【讨论】:

  • 海报特别提到了试图避免将大量参数传递给构造函数,大概是因为这不会给你带来很好的编译时间检查 - 因为如果你传入 5 个整数,编译器无法告诉您是否按正确的顺序获取它们。
  • 但是世界上没有编译器可以告诉我应该输入foo(5, 6) 而不是foo(6, 5)。问题中提出的流畅界面使(略微?)不太可能,但并没有完全消除这种可能性。
  • +1。就在昨天,我正在阅读 Effective Java 的第 2 项(关于使用构建器而不是伸缩式构造器),并且还建议在那里创建具有强制参数的构造器并在正在构建的类实例中进行必要的验证。
  • 你应该使用域对象而不是 5 和 6。
  • “强制用户以一个特定的顺序设置参数” - 非常好。我认为“fluent Builder”或“Bloch Builder”的优点之一是能够以任何你喜欢的顺序设置参数,而失去它会让你向后退一步,只需要一个可伸缩的构造函数。
【解决方案2】:

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

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

【讨论】:

    【解决方案3】:
    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"));
        }
    }
    

    【讨论】:

    • 这是个好主意!通过这种方式,您不会受限于设置参数的特定顺序,但您仍然可以确保设置了所有参数。感谢您提出这个鼓舞人心的想法!
    【解决方案4】:

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

    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 中概述了此模式。

    【讨论】:

    • 因为显然它们都是必需的,而且有很多。如果是这样的话,我想知道其他地方是否还有改进的余地。
    【解决方案5】:

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

    【讨论】:

      【解决方案6】:

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

      【讨论】:

      • Builder 模式的关键在于将它们放入构造函数中是有问题的(跟踪顺序等)。
      • 构建器模式不是将排序与特定步骤分开,这样“构建器”就知道如何执行各个步骤,而“主管”类对它们进行排序吗?也就是说它是模板模式的一个特例。
      【解决方案7】:

      我见过/使用过这个:

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

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

      【讨论】:

        【解决方案8】:

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

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

        【讨论】:

          【解决方案9】:

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

          【讨论】:

            【解决方案10】:

            问题一:关于模式的名字,我喜欢“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

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

            【讨论】:

              猜你喜欢
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 2011-06-13
              • 1970-01-01
              • 2010-11-18
              • 2019-04-14
              • 1970-01-01
              • 1970-01-01
              相关资源
              最近更新 更多