Builderパターン(主にJava)の多くの実装を見てきました。それらすべてにエンティティクラス(Person
クラスとしましょう)とビルダークラスPersonBuilder
があります。ビルダーはさまざまなフィールドを「スタック」し、渡された引数とともに_new Person
_を返します。すべてのビルダーメソッドをPerson
クラス自体に配置するのではなく、明示的にビルダークラスが必要なのはなぜですか?
例えば:
_class Person {
private String name;
private Integer age;
public Person() {
}
Person withName(String name) {
this.name = name;
return this;
}
Person withAge(int age) {
this.age = age;
return this;
}
}
_
私は単にPerson john = new Person().withName("John");
と言うことができます
なぜPersonBuilder
クラスが必要なのですか?
私が目にする唯一の利点は、Person
フィールドをfinal
として宣言できるため、不変性が保証されることです。
immutable AND simulated named parameters を同時に使用できます。
_Person p = personBuilder
.name("Arthur Dent")
.age(42)
.build()
;
_
これは、状態が設定されるまでミットを人から守り、一度設定すると、変更することはできませんが、すべてのフィールドは明確にラベル付けされます。 Javaの1つのクラスだけでこれを行うことはできません。
Josh Blochs Builder Pattern について話しているようです。 Gang of Four Builderパターン と混同しないでください。これらは別の獣です。どちらも構造上の問題を解決しますが、方法はかなり異なります。
もちろん、別のクラスを使用せずにオブジェクトを構築できます。しかし、あなたは選択しなければなりません。名前付きパラメーターを持たない言語(Javaなど)で名前付きパラメーターをシミュレートする機能、またはオブジェクトの存続期間を通じて不変のままでいる機能を失います。
変更できない例、パラメータの名前はありません
_Person p = new Person("Arthur Dent", 42);
_
ここでは、1つの単純なコンストラクターですべてを構築しています。これにより、不変を維持できますが、名前付きパラメーターのシミュレーションが失われます。それは多くのパラメーターで読むのが難しくなります。コンピュータは気にしないが、人間にとっては難しい。
従来のセッターを使用したシミュレーションの名前付きパラメーターの例。不変ではありません。
_Person p = new Person();
p.name("Arthur Dent");
p.age(42);
_
ここでは、すべてをセッターで構築し、名前付きパラメーターをシミュレートしていますが、もはや不変ではありません。セッターを使用するたびにオブジェクトの状態が変化します。
したがって、クラスを追加することで得られるのは、両方を実行できることです。
検証は、存在しない年齢フィールドの実行時エラーで十分であれば、build()
で実行できます。これをアップグレードして、age()
がコンパイラエラーで呼び出されるように強制できます。 Josh Blochビルダー・パターンではありません。
そのためには 内部ドメイン固有言語 (iDSL)が必要です。
これにより、age()
を呼び出す前に、name()
およびbuild()
を呼び出すことを要求できます。しかし、毎回this
を返すだけではできません。返すものごとに、次のものを呼び出すように強制する異なるものを返します。
使用法は次のようになります。
_Person p = personBuilder
.name("Arthur Dent")
.age(42)
.build()
;
_
でもこれは:
_Person p = personBuilder
.age(42)
.build()
;
_
age()
は、name()
から返された型を呼び出す場合にのみ有効であるため、コンパイラエラーが発生します。
これらのiDSLは非常に強力( [〜#〜] jooq [〜#〜] または Java8 Streams など)であり、特に= IDEコード補完ありですが、かなりの設定が必要です。かなりの量のソースコードが書き込まれるような場合のために、保存しておくことをお勧めします。
ビルダークラスを使用/提供する理由:
1つの理由は、渡されたすべてのデータがビジネスルールに従っていることを確認するためです。
あなたの例はこれを考慮していませんが、誰かが空の文字列、または特殊文字で構成される文字列を渡したとしましょう。それらの名前が実際に有効な名前であることを確認することに基づいた何らかのロジックを実行する必要があります(これは実際には非常に難しいタスクです)。
特にロジックが非常に小さい場合(たとえば、年齢が負でないことを確認する場合など)は、すべてをPersonクラスに入れることができますが、ロジックが大きくなるにつれて、それを分離することは理にかなっています。
他の答えで私が見るものとは少し異なる角度でこれについて。
ここでのwithFoo
アプローチはセッターのように動作しますが、クラスが不変性をサポートしているように見えるように定義されているため、問題があります。 Javaクラスでは、メソッドがプロパティを変更する場合、「set」でメソッドを開始するのが慣例です。私はこれを標準としては好きではありませんでしたが、何か他のことをすると、それは- surprise 人、それは良くないここにある基本的なAPIで不変性をサポートできる別の方法があります。次に例を示します。
class Person {
private final String name;
private final Integer age;
private Person(String name, String age) {
this.name = name;
this.age = age;
}
public Person() {
this.name = null;
this.age = null;
}
Person withName(String name) {
return new Person(name, this.age);
}
Person withAge(int age) {
return new Person(this.name, age);
}
}
不適切に作成された部分的に作成されたオブジェクトを防ぐ方法はあまりありませんが、既存のオブジェクトへの変更はできません。これはおそらくこの種のことには愚かなことです(JBビルダーも同様です)。はい、より多くのオブジェクトを作成しますが、これは それほど高価ではありません です。
CopyOnWriteArrayList などの並行データ構造で使用されるこの種のアプローチを主に目にするでしょう。そして、これは不変性が重要である理由を示唆しています。コードをスレッドセーフにする場合、ほとんどの場合、不変性を考慮する必要があります。 Javaでは、各スレッドは可変状態のローカルキャッシュを保持できます。 1つのスレッドが他のスレッドで行われた変更を確認するには、同期ブロックまたは他の同時実行機能を使用する必要があります。これらはいずれもコードにオーバーヘッドを追加します。しかし、変数が最終的なものである場合、何もする必要はありません。値は常に初期化されたものになるため、すべてのスレッドは何があっても同じものを参照します。
他の人が述べたように、オブジェクトを検証するための不変性とすべてのフィールドのビジネスロジックの検証は、別のビルダーオブジェクトの主な理由です。
ただし、再利用性は別の利点です。非常に類似した多くのオブジェクトをインスタンス化したい場合は、ビルダーオブジェクトに小さな変更を加えて、インスタンス化を続行できます。ビルダーオブジェクトを再作成する必要はありません。この再利用により、ビルダーは、多くの不変オブジェクトを作成するためのテンプレートとして機能することができます。これは小さなメリットですが、役に立つものになる可能性があります。
ビルダーは、インターフェースまたは抽象クラスを返すように定義することもできます。ビルダーを使用してオブジェクトを定義できます。ビルダーは、たとえば、設定されているプロパティや設定されているプロパティに基づいて、返す具体的なサブクラスを決定できます。
Builderパターンを使用して、プロパティを設定することにより、オブジェクトを段階的に作成し、すべての必須フィールドが設定されたら、buildを使用して最終オブジェクトを返します方法。新しく作成されたオブジェクトは不変です。ここで注意すべき重要な点は、オブジェクトは最後のビルドメソッドが呼び出されたときにのみ返されるということです。これにより、すべてのプロパティがオブジェクトに設定され、ビルダークラスから返されたときにオブジェクトが不整合な状態にならないことが保証されます。
ビルダークラスを使用せず、すべてのビルダークラスメソッドを直接Personクラス自体に配置する場合は、最初にオブジェクトを作成してから、作成されたオブジェクトでセッターメソッドを呼び出す必要があります。オブジェクトとプロパティの設定。
したがって、ビルダークラス(つまり、Personクラス自体以外の外部エンティティ)を使用することで、オブジェクトが不整合な状態になることはありません。
実際には、クラス自体にビルダーメソッドをcanできますが、それでも不変性があります。これは、既存のオブジェクトを変更するのではなく、ビルダーメソッドが新しいオブジェクトを返すことを意味します。
これは、最初の(有効/有用な)オブジェクトを取得する方法(たとえば、すべての必須フィールドを設定するコンストラクター、またはデフォルト値を設定するファクトリーメソッド)がある場合にのみ機能し、追加のビルダーメソッドは、変更されたオブジェクトに基づいて返します。既存のものに。 これらのビルダーメソッドでは、途中で無効なオブジェクトや一貫性のないオブジェクトが取得されないようにする必要があります。
もちろん、これは多くの新しいオブジェクトが作成されることを意味します。オブジェクトの作成にコストがかかる場合は、これを行わないでください。
テストコードでこれを使用して、ビジネスオブジェクトの1つに Hamcrestマッチャー を作成しました。正確なコードは思い出せませんが、次のようになります(簡略化)。
public class CustomerMatcher extends TypeSafeMatcher<Customer> {
private final Matcher<? super String> nameMatcher;
private final Matcher<? super LocalDate> birthdayMatcher;
@Override
protected boolean matchesSafely(Customer c) {
return nameMatcher.matches(c.getName()) &&
birthdayMatcher.matches(c.getBirthday());
}
private CustomerMatcher(Matcher<? super String> nameMatcher,
Matcher<? super LocalDate> birthdayMatcher) {
this.nameMatcher = nameMatcher;
this.birthdayMatcher = birthdayMatcher;
}
// builder methods from here on
public static CustomerMatcher isCustomer() {
// I could return a static instance here instead
return new CustomerMatcher(Matchers.anything(), Matchers.anything());
}
public CustomerMatcher withBirthday(Matcher<? super LocalDate> birthdayMatcher) {
return new CustomerMatcher(this.nameMatcher, birthdayMatcher);
}
public CustomerMatcher withName(Matcher<? super String> nameMatcher) {
return new CustomerMatcher(nameMatcher, this.birthdayMatcher);
}
}
次に、単体テストでこのように使用します(適切な静的インポートを使用)。
assertThat(result, is(customer().withName(startsWith("Paŭlo"))));
ここで明示的に言及されていないもう1つの理由は、build()
メソッドがすべてのフィールドが 'フィールドに有効な値(直接設定、または他のフィールドの他の値から派生した)が含まれていることを確認できることです。おそらく、そうでなければ発生する可能性が最も高い障害モードです。
もう1つの利点は、Person
オブジェクトのライフタイムがよりシンプルになり、不変のセットがシンプルになることです。 Person p
、 あなたが持っている p.name
および有効なp.age
。 「年齢が設定されていても名前が設定されていない場合、または名前が設定されていて年齢が設定されていない場合」などの状況を処理するようにメソッドを設計する必要はありません。これにより、クラス全体の複雑さが軽減されます。