所以我们得到了两个非常相似的类 [...]。我认为这违反了 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 一个私有构造函数是个好主意。