なぜ型がそのビルダーと結合されるのでしょうか?


20

私は最近、Code Reviewで私の回答を削除しました。

private Person(PersonBuilder builder) {

やめる。赤旗。PersonBuilderはPersonを構築します。それは人について知っています。PersonクラスはPersonBuilderについて何も知らないはずです-それは単なる不変の型です。ここで、Aに依存するBに依存するAに依存する循環カップリングを作成しました。

Personはパラメータを取得するだけです。構築せずにPersonを作成するクライアントは、それを実行できる必要があります。

私は下票で平手打ちされ、それを(引用して)赤旗と言った、なぜ?ここでの実装は、Joshua Blochが彼の「Effective Java」本(アイテム#2)で示したものと同じ形をしています。

したがって、Javaでビルダーパターンを実装する正しい方法は、ビルダーをネスト型にし(これはこの質問の目的ではありません)、製品を作成することです(ビルドされているオブジェクトのクラス)このように、ビルダー依存します:

private StreetMap(Builder builder) {
    // Required parameters
    origin      = builder.origin;
    destination = builder.destination;

    // Optional parameters
    waterColor         = builder.waterColor;
    landColor          = builder.landColor;
    highTrafficColor   = builder.highTrafficColor;
    mediumTrafficColor = builder.mediumTrafficColor;
    lowTrafficColor    = builder.lowTrafficColor;
}

https://en.wikipedia.org/wiki/Builder_pattern#Java_example

同じBuilderパターンの同じWikipediaページには、C#の実装が大きく異なります(そして、はるかに柔軟です)。

//Represents a product created by the builder
public class Car
{
    public Car()
    {
    }

    public int Wheels { get; set; }

    public string Colour { get; set; }
}

ご覧のとおり、ここの製品Builderクラスについて何も知らないため、直接コンストラクター呼び出し、抽象ファクトリー、またはビルダーによってインスタンス化できることに気をつけています-私が理解している限り、創造的なパターンの製品は、それを作成しているものについて何も知る必要はありません

私は、ビルダーパターンを使用して、コンストラクターが多数のオプション引数で肥大化するような型を作り直すことができるという反論(明らかにBlochの本で明らかに防御されている)を受けました。だから、私はこのサイトで少し研究したと思っていたと思っていたものに固執する代わりに、私が疑ったように、この議論は水を保持していないことがわかった。

それで、取引は何ですか?そもそも存在してはならない問題に対して、過剰に設計されたソリューションを考え出すのはなぜですか?ジョシュアブロッホを台座から1分間離すと、2つの具体的なタイプを結合し、それをベストプラクティスと呼ぶ1つの正当な理由を見つけることができますか?

これはすべて、私にとってカーゴカルトプログラミングの続きです。


12
ちょうど暴言というのと、この「質問」におい...
フィリップ・ケンドール

2
@PhilipKendallたぶん少し。しかし、私はすべてのJavaビルダーの実装がなぜそのような密結合を持っているのかを理解することに本当に興味があります。
マチューギンドン

19
@PhilipKendallそれは私に好奇心をかき立てます。
マスト

8
@PhilipKendallそれは暴言のように見えます、はい、しかしその中核には有効な話題に関する質問があります。たぶん暴言を編集することができますか?
アンドレスF.

「コンストラクター引数が多すぎる」という引数には感心しません。しかし、これら両方のビルダーの例にはあまり感心しません。おそらく、例は単純すぎて非自明な振る舞いを示すことができないためですが、Javaの例は複雑な流interfaceなインターフェースのように見え、C#の例はプロパティを実装するための迂遠な方法にすぎません。どちらの例も、どのような形式のパラメーター検証も行いません。コンストラクターは、少なくとも指定された引数のタイプと数を検証します。つまり、引数が多すぎるコンストラクターが勝ちます。
ロバートハーベイ

回答:


22

私はそれが赤旗であるというあなたの主張に同意しません。また、Builderパターンを表す1つの正しい方法があるという、カウンターポイントの表現に反対します。

私に 1つの質問があります:Builderは型のAPIの必要な部分ですか?ここで、PersonBuilderはPerson APIの必要な部分ですか?

妥当性がチェックさ不変、および/または緊密にカプセル化されたPersonクラスをすることを完全に合理的である必然(関わらず、そのビルダーがネストまたは隣接しているかどうかの)それが提供するビルダを使用して作成すること。そうすることで、Personはすべてのフィールドをprivateまたはpackage-privateおよびfinalに保つことができます。また、作業中の場合は、クラスを変更のために開いたままにしておくことができます。(最終的には修正のために閉じられ、拡張のために開かれるかもしれませんが、それは現在重要ではなく、特に不変のデータオブジェクトについては議論の余地があります。)

単一のパッケージレベルAPIデザインの一部として、提供されたPersonBuilderを介してPersonを作成する必要がある場合、循環カップリングは適切であり、ここで赤いフラグは保証されません。特に、あなたはそれを議論の余地のない事実であり、API設計の意見や選択ではないと述べたので、それは悪い反応に貢献したかもしれません。私はあなたをdownvotedていないだろうが、私はそうするために他の誰かのせいにしていない、と私が「何ワラントdownvote」の議論の残り残しておきますコードレビューのヘルプセンターメタ。Downvotesが発生します。「平手打ち」ではありません。

もちろん、Personオブジェクトを作成するために多くの方法を開いたままにしたい場合、PersonBuilderはコンシューマーまたはユーティリティになります。Personクラスは、Personが直接依存するべきではありません。この選択はより柔軟に思えますが(オブジェクト作成オプションをこれ以上必要としない人はいますか?) 。Builderが、多くのオプションフィールドを持つコンストラクターまたは変更に対して開かれているコンストラクターを表現または置換することを意図している場合、クラスの長いコンストラクターは、実装の詳細を隠したままにすることができます。(公式で必要なBuilderは、独自の構築ユーティリティの作成を妨げるものではなく、構築ユーティリティが上記の最初の質問のようにBuilderをAPIとして使用する可能性があることにも留意してください。)


ps:リストしたコードレビューサンプルには不変のPersonがありますが、Wikipediaの反例にはゲッターとセッターを含む可変のCarがリストされています。言語と不変条件の一貫性を保つと、必要な機械が車から省略されているのが見やすくなります。


コンストラクターを実装の詳細として扱うことについて、あなたのポイントを見ていると思います。効果的なJavaがこのメッセージを適切に伝えていないようです(私はその本を持っていない/読んでいません)。 。Wikipediaの例は比較に悪いと思います。しかし、それはWikipedia ..とFWIWです。実際、私は実際にダウン票を気にしません。とにかく、削除された回答は正味の正味スコアで座っています(そして、最終的に[badge:peer-pressure]を獲得するチャンスがあります)。
マチューギンドン

1
しかし、コンストラクター引数多すぎる型は設計上の問題(SRPを破る臭い)であり、ビルダーパターンがカーペットの下にすばやく押し寄せるという考えを揺るがすことはできないようです。
マチューギンドン

3
@ Mat'sMug上記の私の投稿では、クラスが多くのコンストラクター引数を必要とすることを前提としています。同様に、任意のビジネスロジックコンポーネントについて話している場合は、デザインの匂いとして扱いますが、データオブジェクトの場合、タイプが10または20の直接属性を持つことは問題ではありません。たとえば、オブジェクトがlog内の単一の厳密に型指定されたレコードを表すことは、SRPの違反ではありません。
ジェフボーマンは

1
けっこうだ。String(StringBuilder)ただし、コンストラクターは取得できません。これは、型がそのビルダーに依存するのが通常である「原則」に従うようです。そのコンストラクターのオーバーロードを見たとき、私は驚いた。以下のようにそうではありませんString... 42個のコンストラクタのパラメータを持っている
マチューGuindon

3
@ルーアン・オッド。私はそれが文字通り使用されるのを見たことがなく、それが存在することを知りませんでした。toString()普遍的です。
クリリス

7

議論の中で「権威への訴え」が使われたとき、それがどれほど厄介なものになるかを理解しています。議論は独自のIMOに基づいているべきであり、そのような尊敬される人のアドバイスを指すことは間違っていませんが、アリストテレスがそう言ったので、太陽が地球の周りを移動することを知っているので、それ自体では完全な議論とは考えられません。

そうは言っても、あなたの議論の前提は同じだと思います。2つの具体的なタイプを結合してベストプラクティスと呼ぶべきではありません。

カップリングが問題であるという議論が有効であるためには、それが作り出す特定の問題が必要です。このアプローチがこれらの問題の影響を受けない場合、この形式のカップリングに問題があると論理的に主張することはできません。したがって、ここであなたの主張を述べたいのであれば、このアプローチがこれらの問題にどのように影響しているのか、および/またはビルダーを切り離すことで設計がどのように改善されるのかを示す必要があると思います。

率直に言って、私は結合アプローチがどのように問題を引き起こすかを見るのに苦労しています。クラスは完全に結合されていますが、ビルダーはクラスのインスタンスを作成するためだけに存在します。別の見方をすると、コンストラクターの拡張機能として使用できます。


それで...より高い結合、より低い凝集=>ハッピーエンド?うーん...私は「コンストラクタを拡張する」ビルダーについてのポイントを参照してください、しかし、別の例を持ち出すまで、私はものを見るために失敗String取るコンストラクタのオーバーロードを行っているStringBuilderかどうかの議論の余地が(パラメータStringBuilderであるビルダー、それの別の話)。
マチューギンドン

4
@ Mat'sMug明確にするために、私はこのどちらの方法についても本当に強い気持ちを持っていません。多くの状態入力を持つクラスを実際に作成しないため、ビルダーパターンを使用する傾向はありません。他の場所で言及している理由のために、私は一般にそのようなクラスを分解します。私が言っているのは、このアプローチがどのように問題につながるかを示すことができれば、神が降りて石造りのタブレットでモーゼにそのビルダーパターンを渡したかどうかは関係ないということです。あなたは「勝ちます」。一方、できない場合は
...-ジミージェームズ

私が自分のコードベースで持っているビルダーパターンの合法的な使用法の1つは、VBIDE APIをモックすることです。基本的に、コードモジュールの文字列コンテンツを提供し、ビルダーはVBE、提供する文字列を含むモジュールを備えた、ウィンドウ、アクティブなコードペイン、プロジェクト、コンポーネントを備えたモックを提供します。それなしでは、少なくとも私がそれらを見るたびに目を刺すことなく、Rubberduckでテストの80%をどのように書くことができるかわかりません。
マチューギンドン

@ Mat'sMugデータを不変オブジェクトに非整列化するのに非常に便利です。これらの種類のオブジェクトは、プロパティのバッグになる傾向があります。つまり、実際にはオブジェクト指向ではありません。問題の全体の領域は非常に大きなPITAであるため、彼らが良い慣習に従っているかどうかにかかわらず、成し遂げるのに役立つものは何でも慣れるでしょう。
ジミージェームズ

4

型がビルダーと結合される理由に答えるには、そもそもなぜビルダーを使用するのかを理解する必要があります。具体的には、Blochは、コンストラクターパラメーターが多数ある場合はビルダーを使用することをお勧めします。それがビルダーを使用する唯一の理由ではありませんが、私はそれが最も一般的であると推測します。要するに、ビルダーはこれらのコンストラクターパラメーターの代わりであり、多くの場合、ビルダーはビルドしているのと同じクラスで宣言されます。したがって、包含クラスはすでにビルダーの知識を持っているため、コンストラクター引数として渡すことで変更されることはありません。それが、型がそのビルダーと結合される理由です。

多数のパラメーターがあるのは、ビルダーを使用する必要があることを意味するのはなぜですか?さて、多数のパラメーターを持つことは、現実の世界では珍しいことではなく、単一の責任原則に違反するものでもありません。また、10個のStringパラメーターがあり、どのパラメーターがどのパラメーターであり、nullにする必要があるのが5番目または6番目のストリングであるかどうかを記憶しようとしている場合にも、それは面倒です。ビルダーを使用すると、パラメーターの管理が簡単になります。また、本を読んで調べる必要のある他のクールなことができます。

タイプと連動していないビルダーはいつ使用しますか?一般に、オブジェクトのコンポーネントがすぐに利用できず、それらの部分を持つオブジェクトが構築しようとしている型を知る必要がない場合、または制御できないクラスのビルダーを追加する場合。


奇妙なことに、IDEのモックを作成するこのMockVbeBuilderのように、明確な形状を持たない型がある場合にのみ、ビルダーが必要でした。IMO - 1つのテストは、ちょうど別のテストでは、二つの形式とクラスモジュール必要とすることができる、簡単なコードモジュールを必要とするかもしれないのGoFのBuilderパターンが何のためにあるのかです。とはいえ、クレイジーコンストラクターを使用して別のクラスで使用し始めるかもしれませんが、カップリングはまったく正しく感じられません。
マチューギンドン

1
@ Mat's Mugあなたが提示したクラスは、Builderデザインパターン(GammaなどによるDesign Patternsから)をより代表しているようで、必ずしも単純なBuilderクラスではありません。どちらも物を作りますが、同じものではありません。また、ビルダーを「必要」とすることはありません。他のことを簡単にするだけです。
オールドファットネッド

1

創造的なパターンの製品は、それを作成するものについて何も知る必要はありません。

Personこのクラスは、それを作成しているものを知りません。パラメーターを持つコンストラクターのみがあります。パラメーターの型の名前は... Builderで終わりますが、実際には何も強制しません。結局のところ単なる名前です。

このビルダーは、栄光に満ちた1つの構成オブジェクトです。 そして、構成オブジェクトは実際には、互いに属するプロパティをグループ化するだけです。クラスのコンストラクターに渡されるという目的のためだけにそれらが互いに属している場合は、そうしてください!originおよびdestinationStreetMap例は両方ともタイプPointです。各ポイントの個々の座標をマップに渡すことは可能でしょうか?確かに、プロパティはPoint一緒に属します。さらに、構築に役立つあらゆる種類のメソッドを使用できます。

1違いは、ビルダーは単なるデータオブジェクトではなく、これらのチェーンセッター呼び出しを許可することです。主に自分自身を構築する方法に関するものです。Builder

ミックスに少しコマンドパターンを追加しましょう。よく見ると、ビルダーは実際にはコマンドであるためです。

  1. アクションをカプセル化します:別のクラスのメソッドを呼び出す
  2. そのアクションを実行するために必要な情報を保持します:メソッドのパラメーターの値

ビルダーの特別な部分は次のとおりです。

  1. 呼び出されるメソッドは、実際にはクラスのコンストラクターです。今、それを創造的なパターンと呼んでください。
  2. パラメーターの値は、ビルダー自体です

Personコンストラクターに渡されるものはビルドできますが、必ずしもそうする必要はありません。単純な古い構成オブジェクトまたはビルダーの可能性があります。


0

いいえ、それらは完全に間違っており、あなたは絶対に正しいです。そのビルダーモデルは馬鹿げています。コンストラクターの引数は、Personのビジネスには関係ありません。このビルダーはユーザーにとって便利なだけであり、それ以上のものではありません。


4
おかげで...この答えはもう少し拡大すればより多くの票を集めるだろう、それは未完成の答えのように見える=)
マチューギンドン

はい、私は+1、カップリングなしで同じセーフガードと保証を提供するビルダーの代替アプローチ
マット・フリーク

3
これに対するカウンターポイントについては、受け入れられた回答を参照してください。PersonBuilder実際にPersonAPIの重要な部分である場合について言及しています。現状では、この答えはその主張を主張していません。
アンドレスF.
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.