代码之家  ›  专栏  ›  技术社区  ›  tangens

如何改进构建器模式?

  •  39
  • tangens  · 技术社区  · 15 年前

    动机

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

    传统的构建器模式

    当我使用生成器模式创建 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;
                }
            }
        }
    }
    

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

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

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

    …但无法忘记所需的参数。编译器不会接受它。

    可选参数

    如果我有可选参数,我将使用最后一个内部生成器类 Builder4 把它们设置成一个“传统”的建设者所做的,然后自己返回。

    问题

    • 这是众所周知的模式吗?它有特殊的名字吗?
    • 你看到什么陷阱了吗?
    • 您是否有任何想法来改进实现——从减少代码行的意义上来说?
    10 回复  |  直到 7 年前
        1
  •  15
  •   Esko    15 年前

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

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

        2
  •  23
  •   Michael Borgwardt    15 年前

    传统的构建器模式已经处理了这一点:只需在构造函数中使用强制参数即可。当然,没有什么可以阻止调用者传递空值,但您的方法也不能。

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

    另外,这是一项额外的工作。

        3
  •  13
  •   clinux    11 年前
    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
  •  12
  •   Martin    15 年前

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

    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
  •  7
  •   Sled bayer    11 年前

    我不使用多个类,只使用一个类和多个接口。它强制执行您的语法,而不需要太多的输入。它还允许您将所有相关的代码紧密地联系在一起,这样更容易理解代码在更大的级别上发生了什么。

        6
  •  5
  •   JRL    15 年前

    哎呀,这看起来很臃肿。如果你 要拥有所有参数,请在构造函数中传递它们。

        7
  •  5
  •   naXa stands with Ukraine    7 年前

    我见过/使用过:

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

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

        8
  •  2
  •   Community Doug McClean    7 年前

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

    • 你的课可能做得太多了。再次检查它是否不违反 Single Responsibility Principle . 问问自己,为什么需要一个具有如此多必需实例变量的类。
    • 您的构造函数可能是 doing too much . 构造函数的工作是构造。(他们在命名时并没有很有创造性;d)就像类一样,方法有一个单一的责任原则。如果您的构造函数不仅仅是进行字段赋值,那么您需要一个很好的理由来证明这一点。你可能会发现你需要一个 Factory Method 而不是建筑工人。
    • 您的参数可能是 doing too little . 问问你自己,你的参数是否可以被分组成一个小的结构(或者在Java中结构类似的对象)。别害怕上小班。如果你发现你需要做一个结构或小类,不要忘记 to refactor out functionality 它属于结构而不是更大的类。
        9
  •  1
  •   Community Doug McClean    7 年前

    有关更多信息 何时使用构建器模式及其优点 你应该看看我的帖子,再问一个类似的问题 here

        10
  •  0
  •   Community Doug McClean    7 年前

    问题1:关于模式的名称,我喜欢“步骤生成器”的名称:

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

    • 你在执行 序列 你如何使用你的建筑商,这在我的经验中是不寻常的。我知道这在某些情况下是多么重要,但我从来都不需要它。例如,我不认为需要在这里强制执行序列:

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

    • 然而,很多时候,构建者需要强制执行一些约束,以防止构建伪造的对象。也许您希望确保提供了所有必需的字段,或者字段组合是有效的。我猜这是你想把序列引入大楼的真正原因。

      在本例中,我喜欢JoshuaBloch在build()方法中进行验证的建议。这有助于跨域验证,因为此时所有内容都可用。请参阅此答案: https://softwareengineering.stackexchange.com/a/241320

    总之,我不会在代码中添加任何复杂的内容,因为您担心“缺少”对构建器方法的调用。在实践中,这很容易被测试用例捕获。也许从一个普通的构建器开始,然后介绍这个方法,如果你一直因为缺少方法调用而被咬。