web-dev-qa-db-ja.com

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

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として宣言できるため、不変性が保証されることです。

33
Boyan Kushlev

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コード補完ありですが、かなりの設定が必要です。かなりの量のソースコードが書き込まれるような場合のために、保存しておくことをお勧めします。

28
candied_orange

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

  • 不変オブジェクトを作成する-すでに確認したメリット。構築に複数のステップが必要な場合に役立ちます。 FWIW、不変性は、メンテナンス可能でバグのないプログラムを書くための私たちの探求において重要なツールと見なされるべきです。
  • 最終的な(不変の可能性がある)オブジェクトの実行時表現が、読み取りや領域の使用のために最適化されているが、更新のために最適化されていない場合。ここではStringとStringBuilderが良い例です。文字列を繰り返し連結することはあまり効率的ではないため、StringBuilderは、追加に適した別の内部表現を使用します。ただし、スペースの使用には適しておらず、通常のStringクラスのように読み取りおよび使用には適していません。
  • 構築されたオブジェクトと構築中のオブジェクトを明確に区別するため。このアプローチでは、建設中から建設中への明確な移行が必要です。コンシューマーにとって、構築中のオブジェクトと構築されたオブジェクトを混同する方法はありません。型システムがこれを強制します。つまり、このアプローチを使用して、「成功の落とし穴に落ちる」ようにすることができます。また、他の人(または私たち自身)が(APIやレイヤーなどの)使用する抽象化を行う場合、これは非常に便利です。事。
57
Erik Eidt

1つの理由は、渡されたすべてのデータがビジネスルールに従っていることを確認するためです。

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

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

21
Deacon

他の答えで私が見るものとは少し異なる角度でこれについて。

ここでの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つのスレッドが他のスレッドで行われた変更を確認するには、同期ブロックまたは他の同時実行機能を使用する必要があります。これらはいずれもコードにオーバーヘッドを追加します。しかし、変数が最終的なものである場合、何もする必要はありません。値は常に初期化されたものになるため、すべてのスレッドは何があっても同じものを参照します。

6
JimmyJames

Builderオブジェクトを再利用する

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

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

3
yitzih

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

2
Ed Marty

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

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

したがって、ビルダークラス(つまり、Personクラス自体以外の外部エンティティ)を使用することで、オブジェクトが不整合な状態になることはありません。

2

実際には、クラス自体にビルダーメソッドを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"))));
2
Paŭlo Ebermann

ここで明示的に言及されていないもう1つの理由は、build()メソッドがすべてのフィールドが 'フィールドに有効な値(直接設定、または他のフィールドの他の値から派生した)が含まれていることを確認できることです。おそらく、そうでなければ発生する可能性が最も高い障害モードです。

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