web-dev-qa-db-ja.com

ビルダーパターン:いつ失敗するか?

ビルダーパターンを実装するとき、いつビルドを失敗させるかと戸惑うことがよくあり、数日ごとに問題についてさまざまな立場をとることさえできます。

最初にいくつかの説明:

  • failing earlyを使用すると、無効なパラメーターが渡されるとすぐにオブジェクトの構築が失敗するはずです。つまり、SomeObjectBuilderの内部です。
  • failing lateを使用すると、オブジェクトの構築は、構築されるオブジェクトのコンストラクターを暗黙的に呼び出すbuild()呼び出しでのみ失敗する可能性があることを意味します。

次にいくつかの引数:

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

これに関する一般的な合意は何ですか?

47
skiwi

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

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

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

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

SOLIDの観点から、ビルダーに検証を置くこともSRPに違反します:ビルダークラスはオブジェクトを構築するためにデータを集約する責任をすでに持っています。検証はそれ自体の内部状態でコントラクトを確立しています。別のオブジェクトの状態をチェックする新しい責任。

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

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

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

35
Ivan Gammel

Javaを使用している場合、記事のJoshua Blochが提供する信頼できる詳細なガイダンスを検討してください Creating and Destroying Java Objects (下の引用の太字は私のものです):

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

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

この記事の 編集者の説明 によるメモ、上記の引用の「項目」は Effective Java、Second Edition で提示されたルールを参照しています。

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

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

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

34
gnat

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

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

スローされた例外をキャッチする必要があるため、パラメーターの設定時に例外をスローすることは一般的に嫌いです。そのため、build()での検証を優先する傾向があります。したがって、この場合も、RuntimeExceptionを使用することをお勧めします。これも、渡されたパラメータのエラーは通常発生しないはずだからです。

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

19
Neil

私の知る限り、一般的な慣行(コンセンサスがあるかどうかはわかりません)は、できるだけ早くエラーを発見できるように失敗することです。これにより、APIを誤って誤用することがさらに困難になります。

負ではないはずの容量や長さなど、入力時にチェックできる自明な属性の場合は、すぐに失敗するのが最善です。エラーを遅らせると、ミスとフィードバックの間の距離が長くなり、問題の原因を見つけるのが難しくなります。

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

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

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

11
JvR

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

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

プロパティがintrinsically invalidの場合...

CarBuilder.numberOfWheels( -1 ). ...  

...その後、すぐに拒否します。

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

CarBuilder.numberOfWheels( 0 ).type( 'Hovercraft' ). ...  
0
Phill W.