为什么类成员在构建器模式中重复?

时间:2016-06-13 12:00:29

标签: design-patterns

构建器模式(在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;
        }

    }

}

2 个答案:

答案 0 :(得分:3)

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

这里回答了类似的问题:

What makes up a Builder Pattern?

答案 1 :(得分:2)

  

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

这是值得商榷的。毕竟,构建器类中的字段有时可能与产品类中的字段完全不同。即使它们相似,通常也只需要在构建器中表示产品的一部分字段。

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

无论如何,让我们来看看更实际的部分。

  

以下代码有什么问题?

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

相反,您的实现会在每次调用MyClass时返回一个相同的预构建build()实例。在许多情况下,这种行为可能会给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,我应该指出内部构建器类需要声明static,以便外部代码或方便静态方法(getBuilder()MyClass,可以在不需要现有MyClass实例的情况下构建它。 (对于你的问题中的两个实现都是如此,所以我认为缺少static是你的错字。) 另一个小问题:如果您希望客户使用构建器类并且不希望它们直接构造MyClass实例,那么给MyClass私有构造函数是个好主意。