构建器模式(在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;
}
}
}
答案 0 :(得分:3)
使用您建议的代码,如果您尝试使用相同的构建器实例构建多个对象,则始终使用相同的对象,因此每次调用构建方法时都不会创建新对象,只需使用相同的对象时间。实际上,在你的构建方法中,你没有构建任何东西。 Builder模式用于每次构建一个新对象,这就是为什么它以这种方式实现的。
这里回答了类似的问题:
答案 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
私有构造函数是个好主意。