だから我々は[...] 2つの非常に類似したクラスを取得します。これは私の意見ではドライ原則に違反しています。
これは議論の余地があります。結局のところ、ビルダークラスのフィールドは、製品クラスのフィールドとはまったく異なる場合があります。たとえ似ていても、多くの場合、製品のフィールドのサブセットのみがビルダーに表示される必要があります。
DRYの定義(「すべての知識は、システム内で単一で明白で信頼できる表現でなければなりません」)が与えられた場合、共通実装がDRYに違反しないという議論ができます。ビルダーのフィールドは名前が一致しても、実際には2つの異なる知識を表します。前者は既存のオブジェクトの内部状態に関する情報を表し、後者は製品の別のインスタンスを作成するために必要なデータを表します。
とにかく、より実用的な部分に移動しましょう。
次のコードに問題がありますか?
は、一般的に言えば、それは同じビルダーインスタンス上build()
への後続の各呼び出しはMyClass
の新しいインスタンスを作成したり、少なくとも、例外をスローすることを期待するのは、あなたのMyClassBuilder
を使用して、プログラマのための合理的です、ビルダーインスタンスの再利用が実装でサポートされていない場合
代わりに、build()
への呼び出しごとに、事前に作成された1つの同じプリコンパイル済みの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());
次に仮想的なプログラマはすべての製品が実際に1と同じオブジェクト(ビルダーが親切に言及彼らが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
}
}
}
一つは、私たちのビルダーの実装は、より多くの栄光Prototypeのように見えるし始めていることに気づくかもしれません。実際に、プライベートコピーコンストラクタをpublic clone()
メソッドに置き換える場合、MyClass
は文字通りPrototypeの例になります。その時点で、ほとんどの場合、MyClassBuilder
はまったく必要ありません。それでも、ビルダーのそのような実装は正常に動作します。
あなたのアプローチを持つ別の、間違いなく、より多くのマイナー、問題がある:それは、値がfinal
宣言されてから、プロセスを構築時に設定されているMyClass
内のフィールドを、防ぐことができます。あなたが単に上記のフィールドにパブリックセッターを提供しないことを選ぶことができるので、これはクライアントコードにとってあまり重要ではありません。しかし、MyClass
で明示的に宣言されたフィールドを持つことは、MyClass
のコードで作業しているプログラマーが誤って値を変更することを防ぎ、バグを見つけにくくなるため、フィールドを持つことはまだ素晴らしいです。今
、「普通」の実装、あなたはそれを提示するよう、また、この問題に苦しんでいるが、そこに次のようにそれがMyClass
に別のコンストラクタを導入し、ビルダーのbuild()
メソッドを書き換えることによって対処することができます
public MyClass build() {
return new MyClass(nestedMember1, nestedMember2);
}
一般にJavaでBuilderを実装する場合、内側のBuilderクラスをstatic
と宣言する必要があるので、外部コードまたは便利な静的メソッド(getBuilder()
)をMyClass
にすると、既存のインスタンスMyClass
。これはどちらの実装でも当てはまりますので、static
の不足はあなたのタイプミスです) もう一つの小さな点:MyClass
にプライベートコンストラクタを指定することをお勧めします。クライアントがビルダークラスを使用することを望むのであればそれらが直接MyClass
のインスタンスを構築しないようにしてください。