【问题标题】:Why are class members duplicated in the builder pattern?为什么在构建器模式中类成员重复?
【发布时间】:2016-06-14 06:43:10
【问题描述】:

构建器模式(在 Java 中)通常通过如下示例来说明:

public class MyClass {

    private String member1;

    private String member2;

    // Getters & setters

    public class MyClassBuilder {

        private String nestedMember1;

        private String nestedMember2;

        public MyClassBuilder withMember1(String member1) {
            this.nestedMember1 = member1;
            return this;
        }

        public MyClassBuilder withMember2(String member2) {
            this.nestedMember1 = member1;
            return this;
        }

        public MyClass build() {
            MyClass myClass = new MyClass();

            myClass.member1 = nestedMember1;
            myClass.member2 = nestedMember2;

            return myClass;
        }

    }

}

所以我们得到了两个非常相似的类,因为这种模式主要应用于我们有大量成员的情况,所以我们得到了大量的相似之处。我认为这违反了 DRY 原则。

以下代码有什么问题?

public class MyClass {

    private String member1;

    private String member2;

    // Getters & setters

    public class MyClassBuilder {

        private MyClass myClass = new MyClass();

        public MyClassBuilder withMember1(String member1) {
            myClass.member1 = member1;
            return this;
        }

        public MyClassBuilder withMember2(String member2) {
            myClass.member2 = member1;
            return this;
        }

        public MyClass build() {
            return myClass;
        }

    }

}

【问题讨论】:

    标签: design-patterns


    【解决方案1】:

    所以我们得到了两个非常相似的类 [...]。我认为这违反了 DRY 原则。

    这是有争议的。毕竟,builder 类中的字段有时可能与 product 类中的字段完全不同。即使它们相似,通常也只需要在构建器中表示产品字段的子集。

    鉴于 DRY 的定义(“系统中的每条知识都必须有一个单一的、明确的、权威的表示。”),可以说通用实现不违反 DRY,因为产品的字段和构建器的字段字段,即使名称一致,实际上也代表两种不同的知识:前者代表有关现有对象内部状态的信息,而后者代表创建另一个产品实例所需的数据。

    不管怎样,让我们​​进入更实际的部分。

    以下代码有什么问题?

    一般来说,对于使用您的MyClassBuilder 的程序员来说,期望在同一构建器实例上对build() 的每次后续调用都会创建一个MyClass 的新实例,或者至少抛出一个新实例是合理的。如果您的实现不支持构建器实例的重用,则例外。

    相反,您的实现会在每次调用 build() 时返回一个相同的预构造 MyClass 实例。在许多情况下,这种行为可能会给MyClassBuilder 的用户带来一些令人不快的惊喜。考虑以下示例:

    // Example 1
    MyClass.MyClassBuilder builder = new MyClass.MyClassBuilder()
        .withMember1("foo")
        .withMember2("bar");
    
    MyClass product = builder.build();
    
    builder.withMember2("baz");
    
    // The following line prints out "baz", instead of "bar"...
    // But wait, I haven't even touched product!
    System.out.println(product.getMember2());
    
    
    // Example 2
    MyClass.MyClassBuilder builder = new MyClass.MyClassBuilder()
        .withMember1("hello")
        .withMember2("world");
    
    MyClass product1 = builder.build();
    MyClass product2 = builder.build();
    
    product1.setMember1("bye");
    
    // The following line prints out "bye", instead of "hello"...
    // Wait, what??!
    System.out.println(product2.getMember1());
    

    然后,假设的程序员会意识到所有产品实际上都是同一个对象(构建器在每次调用build() 时都会友好地返回引用)。然后他们真的会很生气。

    如果MyClass 支持克隆或具有复制构造函数,您建议的实现实际上可以满足上述期望。在这种情况下,build() 将能够复制MyClassBuilder 的内部MyClass 实例并在每次调用时返回一个新副本。 为了简单起见,我们添加一个私有的拷贝构造函数:

    public class MyClass {
    
        private String member1;
    
        private String member2;
    
        public String getMember1() {
            return member1;
        }
    
        public String getMember2() {
            return member2;
        }
    
        // Getters & setters
    
        private MyClass() {
    
        }
    
        /**
         * Copy constructor to be used by builder.
         * @param other Original MyClass instance.
         */
        private MyClass(MyClass other) {
            // Strings are immutable, so we can simply copy references.
            // In general, you should consider whether a deep copy needs to be performed for a field.
            this.member1 = other.member1;
            this.member2 = other.member2;
        }
    
        public static class MyClassBuilder {
    
            private MyClass myClass = new MyClass();
    
            public MyClassBuilder withMember1(String member1) {
                myClass.member1 = member1;
                return this;
            }
    
            public MyClassBuilder withMember2(String member2) {
                myClass.member2 = member1;
                return this;
            }
    
            public MyClass build() {
                return new MyClass(myClass);    // The client will now get a copy
            }
        }
    }
    

    此时人们可能会注意到我们的 Builder 实现开始看起来越来越像一个美化的Prototype。事实上,如果我们用公共的clone() 方法替换我们的私有复制构造函数,MyClass 将真正成为 Prototype 的一个示例,在那时,在大多数情况下,将不再需要 MyClassBuilder一点也不。不过,这样的 Builder 实现工作得很好。


    您的方法还有一个可以说是更小问题:它阻止了MyClass 中的字段(其值是在构建过程中设置的)被声明为final。这对于客户端代码来说并不是什么大问题,因为您可以简单地选择不为所述字段提供公共设置器。但是,在MyClass 中明确声明为最终的字段仍然很好,因为它可以防止程序员在处理MyClass 的代码时错误地更改它们的值,从而引入难以发现的错误。

    现在,正如您所介绍的,“通用”实现也存在这个问题,但可以通过将另一个构造函数引入 MyClass 并重写构建器的 build() 方法来解决,如下所示:

        public MyClass build() {
            return new MyClass(nestedMember1, nestedMember2);
        }
    

    至于一般用Java实现Builder,我要指出的是,内部builder类需要声明static,这样外部代码,或者MyClass中的一个方便的静态方法(getBuilder()),可以在不需要MyClass 的现有实例的情况下构建它。 (这对于您的问题中的两种实现都是正确的,所以我认为缺少 static 是您的错字。) 另一个小问题:如果您希望客户端使用构建器类并且不希望他们直接构造 MyClass 实例,那么给 MyClass 一个私有构造函数是个好主意。

    【讨论】:

      【解决方案2】:

      使用您建议的代码,如果您尝试使用相同的构建器实例构建多个对象,则您始终使用相同的对象,因此您不会在每次调用 build 方法时创建新对象,而只是使用相同的对象时间。实际上,在您的构建方法中,您没有构建任何东西。 Builder 模式用于每次构建一个新对象,这就是它以这种方式实现的原因。

      这里回答了一个类似的问题:

      What makes up a Builder Pattern?

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2013-02-24
        • 2019-03-08
        • 2019-03-15
        • 1970-01-01
        • 1970-01-01
        • 2014-01-23
        相关资源
        最近更新 更多