Builderパターンを実装するときにBuilderクラスが必要なのはなぜですか?


31

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です。したがって、不変性が保証されます。


4
注:このパターンの通常の名前はchainable setters:D
Mooing Duck

4
単一責任の原則。あなたはPersonクラスにロジックを置く場合、それは何をする複数のジョブを持っている
reggaeguitar

1
どちらの方法でもあなたの利益を得ることができますwithName。名前フィールドのみを変更した人物のコピーを返すことができます。言い換えれば、不変でPerson john = new Person().withName("John");あってもPerson機能します(これは関数型プログラミングの一般的なパターンです)。
ブライアンマックラッチン

9
@MooingDuck:もう1つの一般的な用語は、流れるようなインターフェイスです。
Mac

2
@Mac流れるようなインターフェイスはもう少し一般的だと思います- voidメソッドを持たないようにしてください。したがって、たとえばPerson、名前を出力するメソッドがある場合、Fluent Interfaceを使用してそれを連鎖させることができますperson.setName("Alice").sayName().setName("Bob").sayName()。ところで、私はJavaDocのそれらに正確にあなたの提案で注釈を付けます@return Fluent interface-それreturn thisは実行の最後に行うメソッドに適用するときに十分に一般的で明確であり、かなり明確です。そのため、ビルダーも流interfaceなインターフェイスを使用します。
VLAZ

回答:


27

あなたができるように、それはだ不変名前付きパラメータをシミュレートすると同時に。

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);

ここでは、単一の単純なコンストラクターですべてを構築しています。これにより、不変のままになりますが、名前付きパラメーターのシミュレーションを失います。それは多くのパラメーターで読みにくくなります。コンピュータは気にしませんが、人間にとっては難しいです。

従来のセッターでシミュレートされた名前付きパラメーターの例。不変ではありません。

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は非常に強力(JOOQJava8ストリームなど)で、特にコード補完を備えたIDEを使用する場合は非常に便利ですが、セットアップにはかなりの作業が必要です。それらに対してかなりのソースコードが書かれているもののためにそれらを保存することをお勧めします。


3
そして、誰かが年齢より前に名前を提供しなければならない理由を尋ねると、唯一の答えは「組み合わせ爆発のため」です。ソースにC ++などの適切なテンプレートがある場合、またはすべてに別のタイプを使用する場合は、これを回避できると確信しています。
デュプリケータ

1
漏れやすい抽象化は、それを使用する方法を知っていることができるように根本的な複雑さの知識が必要です。それは私がここで見るものです。自身の構築方法を知らないクラスは、さらに依存関係を持つBuilderに必ず結合されます(Builderコンセプトの目的と性質です)。一度(バンドキャンプではない)オブジェクトをインスタンス化する簡単な必要性は、まさにそのようなクラスターフラックを元に戻すのに3か月を必要としました。子供じゃない
レーダーボブ

構築規則を知らないクラスの利点の1つは、それらの規則が変更されても気にしないということです。AnonymousPersonBuilder独自のルールセットを持つを追加できます。
candied_orange

クラス/システム設計では、「凝集度を最大化し、結合を最小化する」という深い応用を示すことはめったにありません。設計とランタイムの大部分は拡張可能であり、OOの格言とガイドラインがフラクタルであるという感覚で適用された場合のみ、または「カメだらけ」の場合にのみ欠陥を修正できます。
レーダーボブ

1
@radarbob構築されるクラスは、それ自体の構築方法をまだ知っています。そのコンストラクタは、オブジェクトの整合性を保証する責任があります。コンストラクターは多くのパラメーターを受け取ることが多いため、ビルダークラスはコンストラクターの単なるヘルパーです。これは、非常に優れたAPIにはなりません。
いいえU

57

ビルダークラスを使用/提供する理由:

  • 不変オブジェクトを作成するには—既に特定した利点。構築に複数のステップが必要な場合に便利です。FWIW、不変性は、メンテナンス可能なバグのないプログラムを作成するための私たちの探求において重要なツールと見なされるべきです。
  • 最終(不変の可能性がある)オブジェクトのランタイム表現が読み取りおよび/またはスペース使用量に対して最適化されているが、更新用には最適化されていない場合。ここでは、StringとStringBuilderが良い例です。文字列を繰り返し連結することはあまり効率的ではないため、StringBuilderは、追加に適した別の内部表現を使用しますが、スペースの使用には適さず、通常のStringクラスほど読み取りや使用には適していません。
  • 構築されたオブジェクトを構築中のオブジェクトから明確に分離するため。このアプローチでは、建設中から建設中への明確な移行が必要です。消費者にとって、構築中のオブジェクトと構築済みのオブジェクトを混同する方法はありません。型システムがこれを強制します。つまり、このアプローチを使用して「成功の落とし穴」に陥ることがあり、他のユーザー(または自分自身)が使用する(APIやレイヤーなど)ために抽象化する場合、これは非常に良いことです。事。

4
最後の箇条書きについて。したがって、aにPersonBuilderはゲッターがなく、現在の値を検査する唯一の方法は.Build()、aを返すように呼び出すことPersonです。この方法で、.Buildは適切に構築されたオブジェクトを検証できますか?これは、「構築中」オブジェクトの使用を防止するためのメカニズムですか?
アーロンLS

@AaronLS、はい、確かに。複雑な検証をBuild操作に入れて、不良オブジェクトや構築不足のオブジェクトを防ぐことができます。
エリックエイド

2
また、コンストラクター内でオブジェクトを検証することもできます。設定は個人的なものでも、複雑さに依存するものでもかまいません。何を選んでも、重要なことは、不変オブジェクトを取得したら、それが適切に構築され、予期しない値がないことを知っているということです。不正な状態の不変オブジェクトは発生しません。
ウォルフラット

2
@AaronLSビルダーはゲッターを持つことができます。それはポイントではありません。必要はありませんが、ビルダーにゲッターがある場合は、このプロパティがまだ指定されていないときにgetAgeビルダーの呼び出しが返さnullれる可能性があることに注意する必要があります。これとは対照的に、Personこのクラスは、年齢になることはありませんという不変を強制することnullビルダーと実際のオブジェクトは、OPのと同じように、混合されたときには不可能である、new Person() .withName("John")喜んで作成する例、Person年齢なし。これは、セッターが不変式を強制する可能性があるが、初期値を強制できないため、可変クラスにも適用されます。
ホルガー

1
@Holgerのポイントは真実ですが、主にビルダーがゲッターを避けるべきであるという議論をサポートしています。
user949300

21

1つの理由は、渡されたすべてのデータがビジネスルールに従うようにすることです。

この例ではこれを考慮していませんが、誰かが空の文字列または特殊文字で構成される文字列を渡したとしましょう。名前が実際に有効な名前であることを確認することに基づいて、何らかのロジックを実行する必要があります(実際には非常に難しいタスクです)。

特にロジックが非常に小さい場合(たとえば、年齢が負でないことを確認するだけの場合)は、すべてをPersonクラスに入れることができますが、ロジックが大きくなるにつれて、それを分離するのが理にかなっています。


7
問題の例は、単純なルール違反を提供していますのために与えられた年齢がないjohn
Caleth

6
+1そして、ビルダークラスなしで2つのフィールドのルールを同時に実行するのは非常に困難です(フィールドX + Yは10を超えることはできません)。
レジナルドブルー

7
@ReginaldBlue "x + y is even"に縛られているとさらに難しくなります。(0,0)の初期条件はOK、状態(1,1)もOKですが、状態を有効に保ち、(0,0)から(1)に到達する単一の変数更新のシーケンスはありません、1)。
マイケルアンダーソン

これが最大の利点だと思います。他のすべてはいい人です。
ジャコモアルゼッタ

5

これについては、他の回答で見たものとは少し異なる角度です。

withFooここでのアプローチは、セッターのように動作しますが、クラスが不変性をサポートするように見えるように定義されているため、問題があります。Javaクラスでは、メソッドがプロパティを変更する場合、「set」でメソッドを開始するのが習慣です。私は標準としてそれを決して愛していませんでしたが、あなたが何か他のものをするならば、それは人々を驚かせるでしょう、そして、それは良くありません。ここにある基本的な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 Builderも同様です)。はい、より多くのオブジェクトを作成しますが、これはあなたが思うほど高価ではありません。

ほとんどの場合、この種のアプローチはCopyOnWriteArrayListなどの並行データ構造で使用されます。そして、これは不変性が重要である理由を示唆しています。コードをスレッドセーフにしたい場合、ほとんどの場合、不変性を考慮する必要があります。Javaでは、各スレッドは変数状態のローカルキャッシュを保持できます。1つのスレッドが他のスレッドで行われた変更を確認するには、同期ブロックまたは他の同時実行機能を使用する必要があります。これらのいずれも、コードにオーバーヘッドを追加します。しかし、変数が最終的なものであれば、何もする必要はありません。値は常に初期化された値になるため、すべてのスレッドは何があっても同じものを見ます。


2

ここでまだ明示的に言及されていない別の理由は、build()メソッドがすべてのフィールドが「直接設定された、または他のフィールドの他の値から派生した有効な値を含むフィールドである」ことを確認できることです。これはおそらく最も失敗しやすいモードですそうでなければ発生します。

もう1つの利点は、Personオブジェクトのライフタイムがより単純になり、不変式のセットが単純になることです。あなたが持っていることを知っていればPerson p、あなたはを持っていp.nameて有効ですp.age。「年齢は設定されているが名前は設定されていない場合、または名前は設定されているが年齢は設定されていない場合はどうでしょう」などの状況を処理するようにメソッドを設計する必要はありません。これにより、クラスの全体的な複雑さが軽減されます。


「[...]はすべてのフィールドが設定されていることを確認できます[...]」Builderパターンのポイントは、すべてのフィールドを自分で設定する必要がなく、適切なデフォルトにフォールバックできることです。とにかくすべてのフィールドを設定する必要がある場合は、おそらくそのパターンを使用しないでください!
ビンセントサ

@VincentSavard確かに、しかし私の推測では、これが最も一般的な使用法です。いくつかの「コア」値セットが設定されていることを検証し、いくつかのオプション値をデフォルトで設定します(デフォルトの設定、他の値からの値の導出など)。
アレクサンダー-モニカの復活

「すべてのフィールドが設定されていることを確認する」と言う代わりに、これを「すべてのフィールドに有効な値が含まれていることを確認する[他のフィールドの値に依存する場合があります]」(つまり、フィールドは値に応じて特定の値のみを許可する別のフィールドの)。これは各セッターで行うことができますが、オブジェクトが完成してビルドの準備ができるまで例外がスローされることなく、誰かがオブジェクトをセットアップするのは簡単です。
yitzih

構築されるクラスのコンストラクターは、その引数の有効性を最終的に担当します。ビルダーでの検証はせいぜい冗長であり、コンストラクターの要件と簡単に同期しなくなる可能性があります。
いいえU

@TKK確かに。しかし、それらは異なることを検証します。私がBuilderを使用した多くの場合、Builderの仕事は、最終的にコンストラクターに渡される入力を作成することでした。たとえば、ビルダーはa URI、a File、またはaで構成されますFileInputStream、および取得するために提供されたものは何でも用途FileInputStream最終的に引数としてコンストラクタ呼び出しに入ることを
復活モニカ-アレクサンダー

2

ビルダーは、インターフェイスまたは抽象クラスを返すように定義することもできます。ビルダーを使用してオブジェクトを定義できます。ビルダーは、設定されているプロパティや設定されているプロパティなどに基づいて、返される具体的なサブクラスを決定できます。


2

Builderパターンは、プロパティを設定することにより、オブジェクトを段階的に構築/作成するために使用され、すべての必要なフィールドが設定されると、buildメソッドを使用して最終オブジェクトを返します。新しく作成されたオブジェクトは不変です。ここで注意すべき主な点は、最終ビルドメソッドが呼び出されたときにのみオブジェクトが返されることです。これにより、すべてのプロパティがオブジェクトに設定され、ビルダークラスから返されたときにオブジェクトが矛盾した状態にならないことが保証されます。

ビルダークラスを使用せず、すべてのビルダークラスメソッドをPersonクラス自体に直接配置する場合、最初にオブジェクトを作成し、作成されたオブジェクトでセッターメソッドを呼び出す必要があります。オブジェクトのプロパティを設定します。

したがって、ビルダークラス(つまり、Personクラス自体以外の外部エンティティ)を使用することにより、オブジェクトが矛盾した状態にならないようにします。


@DawidOあなたは私のポイントを得た。ここで主に強調しているのは、一貫性のない状態です。そして、それはBuilderパターンを使用する主な利点の1つです。
シュバムボンダルデ

2

ビルダーオブジェクトを再利用する

他の人が述べたように、オブジェクトを検証するための不変性とすべてのフィールドのビジネスロジックの検証は、別個のビルダーオブジェクトの主な理由です。

ただし、再利用性も別の利点です。非常によく似た多くのオブジェクトをインスタンス化する場合は、Builderオブジェクトに小さな変更を加えて、インスタンス化を続行できます。ビルダーオブジェクトを再作成する必要はありません。この再利用により、ビルダーは多くの不変オブジェクトを作成するためのテンプレートとして機能できます。それは小さな利点ですが、有用なものになる可能性があります。


1

実際には、クラス自体にビルダーメソッドを設定できます、それでも不変性は保持されます。これは、既存のオブジェクトを変更するのではなく、ビルダーメソッドが新しいオブジェクトを返すことを意味します。

これは、初期(有効/有用)オブジェクトを取得する方法がある場合(たとえば、すべての必須フィールドを設定するコンストラクター、またはデフォルト値を設定するファクトリーメソッド)にのみ機能し、追加のビルダーメソッドは、既存のもの。 これらのビルダーメソッドは、途中で無効または一貫性のないオブジェクトを取得しないようにする必要があります。

もちろん、これは新しいオブジェクトがたくさんあることを意味し、オブジェクトの作成にコストがかかる場合は、これを行うべきではありません。

ビジネスオブジェクトの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
これは本当です-そしてそれは非常に重要なポイントだと思いますが、最後の問題を解決しません-それはあなたが構築されたときにオブジェクトが一貫した状態にあることを検証する機会を与えません。構築したら、ビジネスメソッドの前にバリデーターを呼び出すなどのトリックが必要になります。これにより、呼び出し元がすべてのメソッドを設定するようにします(これは恐ろしい習慣です)。このようなことを通して、なぜBuilderパターンを使用するのかを理解するのは良いことだと思います。
ビルK

@BillK true-本質的に不変のセッター(毎回新しいオブジェクトを返す)を持つオブジェクトは、返される各オブジェクトが有効であることを意味します。無効なオブジェクト(を持っているnameが、持っていない人など)を返す場合age、概念は機能しません。まあ、あなたは実際にはPartiallyBuiltPersonそれが有効ではない間に何かを返すかもしれませんが、ビルダーを隠すハックのようです。
VLAZ

@BillKこれは、「初期(有効/有用な)オブジェクトを取得する方法がある場合」で私が意図したものです。初期オブジェクトと各ビルダー関数から返されるオブジェクトの両方が有効/一貫していると仮定します。このようなクラスの作成者としてのあなたの仕事は、それらの一貫した変更を行うビルダーメソッドのみを提供することです。
パエロエベルマン
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.