Tell Do n't AskとCommand Query Separationの選択方法


25

Tell Do n't Askの原則は次のとおりです。

オブジェクトに何をしたいかを伝えるよう努力する必要があります。彼らに彼らの状態についての質問をしないで、決定を下して、そして、彼らに何をすべきかを彼らに話します。

問題は、呼び出し元として、呼び出されたオブジェクトの状態に基づいて決定を下すべきではないということです。その結果、オブジェクトの状態が変更されます。実装しているロジックは、おそらく呼び出されたオブジェクトの責任であり、あなたの責任ではありません。オブジェクトの外側で決定を下すと、そのカプセル化に違反します。

「聞かないで」の簡単な例

Widget w = ...;
if (w.getParent() != null) {
  Panel parent = w.getParent();
  parent.remove(w);
}

tellバージョンは...

Widget w = ...;
w.removeFromParent();

しかし、removeFromParentメソッドからの結果を知る必要がある場合はどうでしょうか?私の最初の反応は、removeFromParentを変更して、親が削除されたかどうかを示すブール値を返すことでした。

しかし、私はこれをしないと言うコマンドクエリ分離パターンに出くわしました。

すべてのメソッドは、アクションを実行するコマンド、または呼び出し側にデータを返すクエリのいずれかである必要がありますが、両方ではないことを示しています。言い換えれば、質問をしても答えは変わらないはずです。より正式には、メソッドは参照的に透明であり、したがって副作用がない場合にのみ値を返す必要があります。

これら2つは本当に対立しているのですか?どうすれば2つを選択できますか?これについては、Pragmatic ProgrammerまたはBertrand Meyerを使用しますか?


1
ブール値で何をしますか?
デビッド

1
それは、使いやすさをバランスさせずにコーディングパターンにすぎあなたのようにしているダイビングの音
Izkata

ブール値に関して...
ダコタノース

2
私のポイントは、あなたが「何かを返す」ことに集中するのではなく、あなたがそれで何をしたいかに焦点を当てるべきだということです。別のコメントで述べたように、失敗に焦点を合わせて、例外を使用し、操作が完了したときに何かをしたい場合は、コールバックまたはイベントを使用します。なぜ私はあなたのニーズを求めているのですか。何かを返す必要がある具体的な例が見つからない場合は、2つを選択する必要がない可能性があります。
デビッド

1
コマンドクエリの分離は、パターンではなく原則です。パターンは、問題を解決するために使用できるものです。原則は、問題を引き起こさないために従うべきものです。
candied_orange

回答:


25

実際、問題の例はすでに分解の欠如を示しています。

少し変更してみましょう。

Book b = ...;
if (b.getShelf() != null) 
    b.getShelf().remove(b);

これは実際には違いはありませんが、欠陥をより明確にします。なぜ本はその棚について知っているのでしょうか?簡単に言えば、そうすべきではありません。本の棚への依存性(意味をなさない)を導入し、循環参照を作成します。それはすべて悪いです。

同様に、ウィジェットがその親を知る必要はありません。「わかりましたが、ウィジェット自体を適切にレイアウトするためには、ウィジェットの親が必要です。」そして、内部では、ウィジェットは親を知っており、それらに基づいて独自のメトリックを計算するためにメトリックを要求します。tellによるとこれは間違っていると要求しないでください。親は、すべての子にレンダリングするよう指示し、必要なすべての情報を引数として渡す必要があります。この方法では、2つの親に同じウィジェットを簡単に配置できます(実際に意味があるかどうかは関係ありません)。

本の例に戻るには、司書を入力してください。

Librarian l = ...;
Book b = ...;
l.findShelf(b).remove(b);

言う意味では、私たちはもう尋ねていないことを理解してください、尋ねないでください

最初のバージョンでは、棚は本の所有物であり、あなたはそれを求めました。2番目のバージョンでは、本についてそのような仮定を行いません。私たちが知っているのは、司書が本を含む棚を教えてくれるということだけです。おそらく彼は何らかのルックアップテーブルに依存しますが、私たちは確実に知りません(すべての棚のすべての本をループすることもできます)。実際には知りたくありません
図書館員の応答が、その状態または依存する他のオブジェクトの状態に結合しているかどうか、またはその方法に依存しません。図書館員に棚を見つけるように伝えます。
最初のシナリオでは、本の状態の一部として本と棚の関係を直接エンコードし、この状態を直接取​​得しました。本から返された棚には本が含まれていると仮定しているため、これは非常に脆弱です(これは、私たちが確実にしなければならない制約ですremove

司書の紹介により、この関係を個別にモデル化するため、関心の分離を達成します。


これは非常に有効なポイントだと思いますが、質問にはまったく答えていません。あなたのバージョンでは、Shelfクラスのremove(Book b)メソッドに戻り値が必要ですか?
スカーフリッジ

1
@scarfridge:結論として、質問しないでください、懸念の適切な分離の結果です。あなたがそれについて考えるなら、私はこのように質問に答えます。少なくともそう思う;)
back2dos

1
@scarfridge戻り値の代わりに、失敗時に呼び出される関数/デリゲートを渡すことができます。成功すれば完了ですよね?
ヤフーを祝福12

2
@BlessYahuそれは私には過剰に設計されているようです。個人的には、コマンドとクエリの分離は、実際の(マルチスレッド)世界では理想的だと感じています。私見では、メソッド名がオブジェクトの状態を変更することを明確に示している限り、副作用を持つメソッドが値を返すことは問題ありません。スタックのpop()メソッドを検討してください。ただし、クエリ名を持つメソッドには副作用がありません。
スカーフリッジ

13

結果を知る必要がある場合は、そうします。それがあなたの要件です。

「メソッドは、参照的に透明であり、したがって副作用がない場合にのみ値を返す必要があります」というフレーズは、従うのに適したガイドラインです(特に、並行性またはその他の理由で機能的なスタイルでプログラムを記述している場合)。絶対ではありません。

たとえば、ファイルの書き込み操作の結果を知る必要がある場合があります(trueまたはfalse)。これは値返すメソッドの例ですが、常に副作用が発生します。それを回避する方法はありません。

コマンド/クエリの分離を実現するには、1つのメソッドでファイル操作を実行してから、別のメソッドで結果を確認する必要があります。 ファイルの呼び出しとステータスチェックの間にオブジェクトの状態に何かが起こったらどうなりますか?

一番下の行はこれです:メソッド呼び出しの結果を使用してプログラムの他の場所で決定を下す場合 Tell Do n't Askに違反していません。一方、オブジェクトのメソッド呼び出しに基づいてオブジェクトの決定を行っている場合は、カプセル化を維持するためにそれらの決定をオブジェクト自体移動する必要があります

これは、コマンドクエリ分離とまったく矛盾していません。実際、ステータスの目的で外部メソッドを公開する必要がなくなったため、さらに強制されます。


1
あなたの例では、「書き込み」操作が失敗した場合、例外をスローする必要があるため、「ステータスの取得」メソッドは必要ないと主張できます。
デビッド

@David:次に、OPの例に戻ります。実際に変更が発生した場合はtrue、発生しなかった場合はfalseを返します。そこに例外を投げたいとは思わない。
ロバートハーヴェイ

OPの例でさえ、私にとって適切ではないと感じています。削除が不可能な場合は通知を受け取りたい場合は例外を使用し、ウィジェットが実際に削除された場合はイベントまたはコールバックメカニズムを使用できる場合に通知する必要があります。私はちょうどあなたが「コマンドクエリ分離パターン」を尊重したいとは思わないでしょう実際の例が表示されない
デヴィッド・

@David:答えを明確にするために編集しました。
ロバートハーヴェイ

細かく言ってはいけませんが、コマンドとクエリの分離の背後にある考え方は、ファイルを書き込むコマンドを発行する人は誰でも結果を気にしないということです-少なくとも、特定の意味ではありません(ユーザーは後ですべての最近のファイル操作のリストをプルアップしますが、それは最初のコマンドとは関係ありません)。結果を気にするかどうかに関係なく、コマンドが完了した後にアクションを実行する必要がある場合、それを行う正しい方法は、元のコマンドおよび/またはその詳細を含むイベントを使用する非同期プログラミングモデルを使用することです結果。
アーロンノート

2

最初の本能で行くべきだと思います。時々、デザインは意図的に危険であり、オブジェクトと通信するときにすぐに複雑さを導入して、オブジェクト自体でそれを処理し、すべての厄介な詳細を非表示にしてきれいにするのを難しくするのではなく、すぐに適切に処理するようにする必要があります基礎となる仮定を変更します。

オブジェクトが同等の動作openclose動作を実装し、単純なアトミックタスクだと思われるもののブール値の戻り値が表示されたら、すぐに対処方法を理解し始めると、沈没感を感じるはずです。

後でどう対処するかはあなた次第です。その上に抽象化、を使用したウィジェットタイプを作成できますが、removeFromParent()常に低レベルのフォールバックが必要です。

すべてを単純にしようとしないでください。あなたがエレガントで無邪気に見えるものに頼るとき、それが本当に最悪の瞬間に本当の恐怖であることを理解するだけで、失望するようなものはありません。


2

あなたが説明するのは、コマンドとクエリの分離の原則に対する既知の「例外」です。

この記事では、Martin Fowlerがこのような例外をどのように脅かすかを説明しています。

Meyerはコマンドとクエリの分離を絶対に使用することを好みますが、例外もあります。スタックをポップすることは、状態を変更するクエリの良い例です。Meyerは、この方法を避けることができると正しく述べていますが、これは便利なイディオムです。だから、できるときはこの原則に従うことを好みますが、ポップを得るためにそれを破る用意ができています。

あなたの例では、同じ例外を検討します。


0

コマンドとクエリの分離は、驚くほど簡単に誤解されます。

私のシステムでは、クエリから値を返すコマンドがあり、「ハ!あなたは違反している!」あなたは銃をジャンプしています。

いいえ、それは禁止されていません。

禁止されているのは、そのコマンドがクエリを実行する唯一の方法である場合です。いいえ。質問するために状態を変更する必要はありません。それは、状態を変えるたびに目を閉じなければならないという意味ではありません。

とにかくそれらを再び開くだけなので、私がやっても構いません。この過剰反応の唯一の利点は、設計者が怠zyにならず、状態変化のないクエリを含めることを忘れないことです。

本当の意味は非常に微妙なので、怠け者はセッターがvoidを返すべきだと言う方が簡単です。これにより、実際のルールに違反していないことを確認しやすくなりますが、実際の痛みになりかねない過剰な反応です。

流FluなインターフェイスとiDSLは常にこの過剰反応を「違反」します。あなたが過剰に反応した場合、無視されている多くの力があります。

コマンドは1つのことだけを実行し、クエリは1つのことだけを実行する必要があると主張するかもしれません。そして多くの場合、それは良い点です。コマンドの後に続くクエリは1つだけだと言うのは誰ですか?結構ですが、それはコマンドとクエリの分離ではありません。それが唯一の責任原則です。

このように見て、スタックポップは奇妙な例外ではありません。それは単一の責任です。ピークは、ピークの提供を忘れた場合にのみコマンドクエリの分離に違反します。

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