首页 文章

如何改善构建模式?

提问于
浏览
39

动机

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

传统建筑师模式

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

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 来设置它们像"traditional"构建器那样,返回自己 .

问题

  • 这是一个众所周知的模式吗?它有一个特殊的名字吗?

  • 你看到任何陷阱吗?

  • 您是否有任何改进实施的想法 - 从代码行数较少的角度来看?

10 回答

  • 2

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

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

  • 1

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

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

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

  • 0
    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"));
        }
    }
    
  • 5

    为什么不在构建器构造函数中放入“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中列出 .

  • 23

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

  • 12

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

  • 7

    我见过/用过这个:

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

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

  • 5

    当您有许多可选参数时,通常会使用Builder Pattern . 如果您发现需要许多必需参数,请首先考虑以下选项:

    • 你的 class 可能做得太多了 . 仔细检查它是否违反了Single Responsibility Principle . 问问自己为什么需要一个包含这么多必需实例变量的类 .

    • 您的构造函数可能是doing too much . 构造函数的作用是构造 . (当他们命名时,他们没有变得非常有创意; D)就像课程一样,方法有单一责任原则 . 如果您的构造函数不仅仅是字段赋值,那么您需要有充分的理由来证明这一点 . 您可能会发现需要Factory Method而不是Builder .

    • 您的参数可能是doing too little . 问问自己,您的参数是否可以分组为一个小结构(或Java的结构类对象) . 唐't be afraid to make small classes. If you do find you need to make a struct or small class, don'忘记属于结构而不是你的大类的to refactor out functionality .

  • 13

    有关 when to use the Builder Pattern and its advantages 的更多信息,请查看我的帖子对于另一个类似的问题 here

  • 15

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

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

    • 您正在强制执行 sequence 如何使用您的构建器,这在我的经验中是不寻常的 . 我可以看到在某些情况下这会是多么重要,但是我认为需要在这里强制执行一个序列:

    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开始,然后介绍这个,如果你不断被丢失的方法调用 .

相关问题