Builderパターン:いつ失敗するか?


45

Builderパターンを実装するとき、私はしばしば、ビルドを失敗させるタイミングに戸惑いを感じ、数日ごとにその問題について異なる立場を取ることさえできます。

最初にいくつかの説明:

  • 初期の失敗私は、無効なパラメータが渡されるように、オブジェクトを構築するとすぐに失敗することを意味しています。だから、内側SomeObjectBuilder
  • 後半に失敗し、私は唯一のオブジェクトを構築する上で失敗する可能性があることを意味build()暗黙オブジェクトのコンストラクターを構築するために呼び出すコール。

次に、いくつかの引数:

  • 失敗を遅らせるために:ビルダークラスは、単に値を保持するクラスにすぎないようにします。さらに、コードの重複が少なくなります。
  • 早期の失敗を支持する:ソフトウェアプログラミングの一般的なアプローチは、できるだけ早く問題を検出することです。したがって、チェックする最も論理的な場所は、ビルダークラスのコンストラクター、「セッター」、最終的にはビルドメソッドです。

これについての一般的なコンセンサスは何ですか?


8
失敗することには何の利点もありません。誰かがビルダークラスを「すべき」と言うことは、優れた設計よりも優先されません。
ドーバル

3
これを見るもう1つの方法は、ビルダーが有効なデータを知らない可能性があることです。この場合、早期に失敗するということは、エラーがあることを知ってすぐに失敗することです。 ていない初期の失敗、返すビルダーになるnull問題があったときにオブジェクトをbuild()
クリス

警告を発行する方法を追加せず、ビルダー内で修正する手段を提供しない場合、遅れることは意味がありません。
マーク14年

回答:


34

検証コードを配置できるオプションを見てみましょう。

  1. ビルダーのセッター内。
  2. build()メソッド内。
  3. 構築されたエンティティ内:build()エンティティが作成されるときにメソッドで呼び出されます。

オプション1を使用すると、問題をより早期に検出できますが、完全なコンテキストのみを持つ入力を検証できる複雑なケースがあるため、build()メソッドの検証の少なくとも一部を実行できます。したがって、オプション1を選択すると、検証の一部が1つの場所で実行され、別の部分が別の場所で実行されるという矛盾したコードになります。

通常、ビルダーのセッターはbuild()、特に流invokedなインターフェイスの直前に呼び出されるため、オプション2はオプション1よりもそれほど悪くはありません。したがって、ほとんどの場合、問題を早期に検出することは可能です。ただし、ビルダーがオブジェクトを作成する唯一の方法ではない場合、オブジェクトを作成するすべての場所でビルダーを使用する必要があるため、検証コードの重複につながります。この場合の最も論理的な解決策は、作成されたオブジェクトに可能な限り近い場所、つまり内部に検証を配置することです。そして、これがオプション3です。

SOLIDの観点からすると、ビルダーに検証を行うこともSRPに違反します。ビルダークラスは、データを集約してオブジェクトを構築する責任を既に負っています。検証は、独自の内部状態でコントラクトを確立することであり、別のオブジェクトの状態を確認することは新しい責任です。

したがって、私の観点からは、設計の観点から遅れて失敗する方が良いだけでなく、ビルダー自体ではなく、構築されたエンティティ内で失敗する方が良いです。

UPD:このコメントは、ビルダー(オプション1または2)内の検証が意味をなす場合、もう1つの可能性を思い出しました。ビルダーが、作成しているオブジェクトに独自のコントラクトを持っている場合、それは理にかなっています。たとえば、番号範囲のリストなど、特定のコンテンツを含む文字列を構築するビルダーがあるとします1-2,3-4,5-6。このビルダーにはのようなメソッドがありaddRange(int min, int max)ます。結果の文字列は、これらの数値について何も知りません。また、知る必要もありません。ビルダー自体は、文字列の形式と数値の制約を定義します。したがって、メソッドaddRange(int,int)は入力数を検証し、maxがminより小さい場合に例外をスローする必要があります。

ただし、一般的なルールは、ビルダー自体によって定義された契約のみを検証することです。


私はそれの価値が一方であることを指摘だと思います。オプション1は、回をチェックし、「矛盾」につながる可能性があり、すべてがある場合、それはまだ一貫として見ることができる「可能な限り早期に。」ビルダーのバリアントであるStepBuilderを代わりに使用すると、「できるだけ早く」より明確にするのが少し簡単になります。
ジョシュアテイラー

null文字列が渡された場合にURIビルダーが例外をスローする場合、これはSOLIDの違反ですか?酷い
Gusdor

@Gusdorはい、例外自体をスローする場合。ただし、ユーザーの観点から見ると、すべてのオプションはビルダーによって例外がスローされたように見えます。
イヴァンガンメル

それでは、なぜbuild()によって呼び出されるvalidate()がないのでしょうか?そうすれば、重複、一貫性はほとんどなく、SRP違反はありません。また、ビルドを試行せずにデータを検証することもでき、検証は作成に近いものです。
StellarVortex

この場合の@StellarVortexは2回検証されます-builder.build()で1回検証され、データが有効でオブジェクトのコンストラクターに進む場合はそのコンストラクターで検証されます。
イヴァンガンメル14年

34

Javaを使用していることを考えると、記事「Javaオブジェクトの作成と破棄」でJoshua Blochが提供する信頼できる詳細なガイダンスを検討してください(以下の引用の太字は私のものです)。

コンストラクターと同様に、ビルダーはパラメーターに不変条件を課すことができます。buildメソッドはこれらの不変条件をチェックできます。パラメーターをビルダーからオブジェクトにコピーした後にチェックすること、およびビルダーフィールドではなくオブジェクトフィールドでチェックすることが重要です(項目39)。不変条件に違反する場合、ビルドメソッドはIllegalStateException(Item 60)をスローする必要があります。例外の詳細メソッドは、どの不変式が違反されているかを示す必要があります(項目63)。

複数のパラメーターを含む不変条件を課す別の方法は、セッターメソッドに、不変式が保持する必要があるパラメーターのグループ全体を取得させることです。不変条件が満たされない場合、セッターメソッドはをスローしIllegalArgumentExceptionます。これには、ビルドが呼び出されるのを待つのではなく、無効なパラメーターが渡されるとすぐに不変の障害を検出するという利点があります。

この記事の編集者の説明によると、上記の引用の「項目」は、Effective Java、Second Editionで提示された規則を指します。

この記事では、これが推奨される理由について詳しく説明していませんが、考えてみれば、その理由は明らかです。これを理解するための一般的なヒントは、記事で、ビルダーの概念がコンストラクターの概念にどのように接続されているのかという説明で提供されています。クラスの不変式は、呼び出しの前に/準備する可能性のある他のコードではなく、コンストラクターでチェックされることが期待されています。

ビルドを呼び出す前に不変条件をチェックすることが間違っている理由をより具体的に理解するために、CarBuilderの一般的な例を検討してください。Builderメソッドは任意の順序で呼び出すことができ、その結果、特定のパラメーターがビルドまで有効かどうかを実際に知ることはできません。

スポーツカーは2席までしか持てないことを考慮して、setSeats(4)大丈夫かどうかをどうやって知ることができますか?setSportsCar()呼び出されたかどうか、つまりスローするかどうかを確実に知ることができるのは、ビルド時のみTooManySeatsExceptionです。


3
スローする例外の種類、正確に私が探していたものを推奨するために+1。
Xantix

代わりになるかどうかはわかりません。不変式をグループでのみ検証できる場合についてのみ話しているようです。ビルダーは、他の属性が含まれていない場合は単一の属性を受け入れ、グループに不変式がある場合のみ属性のグループを受け入れます。この場合、単一の属性がビルドの前に例外をスローする必要がありますか?
ディディエA.

19

私の意見では、許容されないため無効な無効な値はすぐに知らせてください。つまり、正の数のみを受け入れ、負の数が渡される場合、build()呼び出されるまで待つ必要はありません。これらは、最初にメソッドを呼び出すための前提条件であるため、「予想される」タイプの問題が発生するとは考えません。つまり、特定のパラメーターの設定の失敗に依存する可能性は低いでしょう。パラメータが正しいと推測するか、自分でいくつかのチェックを行う可能性が高くなります。

ただし、検証が容易ではないより複雑な問題の場合は、を呼び出しbuild()たときに通知される方がよい場合があります。この良い例は、提供した接続情報を使用してデータベースへの接続を確立することです。この場合、技術的にそのような状態をチェックできますが、もはや直感的ではなく、コードが複雑になるだけです。私が見るように、これらは実際に発生する可能性のある種類の問題であり、実際に試してみるまで本当に予測することはできません。文字列をintとして解析できるかどうかを確認するために正規表現と文字列を照合することと、結果として発生する可能性のある例外を処理することを単に解析することとの違いです。

スローされた例外をキャッチしなければならないことを意味するため、パラメーターを設定するときに例外をスローすることは一般的に嫌いbuild()です。したがって、この理由から、RuntimeExceptionを使用することをお勧めします。これは、渡されたパラメーターのエラーが通常は発生しないためです。

ただし、これは何よりもベストプラクティスです。それがあなたの質問に答えることを願っています。


11

私の知る限り、一般的な方法(コンセンサスがあるかどうかはわかりません)は、エラーを発見できる限り早く失敗することです。これにより、意図せずにAPIを誤用することも難しくなります。

負でないはずの容量や長さなど、入力で確認できる些細な属性の場合は、すぐに失敗するのが最善です。エラーを抑えると、ミスとフィードバックの距離が長くなり、問題の原因を見つけるのが難しくなります。

属性の有効性が他の属性に依存する状況にいるという不幸がある場合、2つの選択肢があります。

  • 両方(またはそれ以上)の属性を同時に指定する必要があります(つまり、単一のメソッド呼び出し)。
  • 変更が着信しなくなったことがわかったら、すぐに有効性をテストしますbuild()

ほとんどの場合と同様に、これはコンテキストで行われた決定です。コンテキストが早期に失敗することを厄介または複雑にする場合、トレードオフを行ってチェックを後回しにすることができますが、フェイルファーストがデフォルトである必要があります。


要約すると、オブジェクト/プリミティブ型でカバーされていた可能性のあるすべてのものをできるだけ早く検証するのが妥当であると言っていますか?同様にunsigned@NonNullなど
skiwi

2
@skiwiかなり、はい。ドメインチェック、ヌルチェック、そのようなこと。それ以上のことを提唱するつもりはありません。ビルダーは一般に単純なものです。
JvR

1
あるパラメーターの有効性が別のパラメーターの値に依存する場合、他のパラメーターが「実際に」確立されていることがわかっている場合にのみ、パラメーター値を正当に拒否できることに注意してください。パラメータ値を複数回設定できる場合(最後の設定が優先)、場合によっては、オブジェクトを設定する最も自然な方法Xは、現在の値が与えられたときに無効な値にパラメータを設定することですY。しかし、有効にする値にbuild()設定Yを呼び出す前にX
supercat

例えば、1つは、構築された場合ShapeやビルダーはありWithLeftかつWithRightそれが必要、別の場所にオブジェクトを構築するビルダーを調整するためのプロパティ、および1つの希望WithRightのオブジェクトを右に移動すると、ときに最初に呼び出されるWithLeft不要な複雑さを追加することになり、それが左に移動するとき許可と比較WithLeftすることを提供し、古い右端の右に左のエッジを設定するためにWithRight修正右端が前にbuild呼ばれています。
supercat

0

基本的なルールは「早期失敗」です。

やや高度なルールは、「できるだけ早く失敗する」です。

プロパティが本質的に無効な場合...

CarBuilder.numberOfWheels( -1 ). ...  

...すぐに拒否します。

他のケースでは、値を組み合わせてチェックする必要があり、build()メソッドに配置する方が適切な場合があります。

CarBuilder.numberOfWheels( 0 ).type( 'Hovercraft' ). ...  
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.