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

ste*_*oss 5 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;
        }

    }

}
Run Code Online (Sandbox Code Playgroud)

所以我们得到了两个非常相似的类,并且由于这种模式主要在我们拥有大量成员时应用,因此我们得到了大量相似之处。在我看来,这违反了 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;
        }

    }

}
Run Code Online (Sandbox Code Playgroud)

Ter*_*ass 5

所以我们得到了两个非常相似的类[...]。我认为这违反了 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());
Run Code Online (Sandbox Code Playgroud)

然后,假设的程序员将意识到所有产品实际上都是同一个对象(构建器每次调用时都友好地返回引用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
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

此时,人们可能会注意到我们的 Builder 实现开始看起来越来越像一个美化的Prototype。事实上,如果我们用公共clone()方法替换私有复制构造函数,MyClass那么它实际上就会成为 Prototype 的一个例子,到那时,在大多数情况下,就不再需要了MyClassBuilder。尽管如此,Builder 的这种实现仍然可以正常工作。


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

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

    public MyClass build() {
        return new MyClass(nestedMember1, nestedMember2);
    }
Run Code Online (Sandbox Code Playgroud)

至于一般在 Java 中实现 Builder,我应该指出,需要声明内部构建器类static,以便外部代码或getBuilder()中的方便静态方法 ()MyClass可以构造它,而不需要现有的MyClass. (对于您问题中的两种实现都是如此,所以我认为缺少static是您的拼写错误。)另一个小点:MyClass如果您更喜欢客户端使用构建器类并且不使用私有构造函数,那么最好提供一个私有构造函数。不希望他们直接构造MyClass实例。