「教えてはいけない」が良いオブジェクト指向とみなされる方法の説明


49

このブログ投稿は、いくつかの賛成票とともにHacker Newsに投稿されました。C ++から来るこれらの例のほとんどは、私が教えてきたものに反するようです。

例#2など:

悪い:

def check_for_overheating(system_monitor)
  if system_monitor.temperature > 100
    system_monitor.sound_alarms
  end
end

良い対:

system_monitor.check_for_overheating

class SystemMonitor
  def check_for_overheating
    if temperature > 100
      sound_alarms
    end
  end
end

C ++でのアドバイスは、カプセル化を増やすため、メンバー関数ではなくフリー関数を好むことです。これらは両方とも意味的に同一なので、なぜより多くの状態にアクセスできる選択肢を好むのでしょうか?

例4:

悪い:

def street_name(user)
  if user.address
    user.address.street_name
  else
    'No street name on file'
  end
end

良い対:

def street_name(user)
  user.address.street_name
end

class User
  def address
    @address || NullAddress.new
  end
end

class NullAddress
  def street_name
    'No street name on file'
  end
end

User無関係のエラー文字列をフォーマットするのはなぜですか?'No street name on file'ストリートがない場合に印刷以外の何かをしたい場合はどうすればよいですか?通りの名前が同じものである場合はどうなりますか?


誰かに「教えて、聞かないで」の利点と理論的根拠を教えてもらえますか?どちらが良いかを探しているのではなく、著者の視点を理解しようとしています。


コード例はPythonではなくRubyかもしれません、私は知らない。
パブ

2
最初の例のようなものが、むしろSRPの違反ではないのだろうか?
stijn

1
あなたはそれを読むかもしれません:pragprog.com/articles/tell-dont-ask
Mik378

ルビー。@は省略形であり、Pythonはブロックを暗黙的に空白で終了します。
エリックReppen

3
「C ++でのアドバイスは、カプセル化を増やすため、メンバー関数ではなくフリー関数を好むことです。」誰があなたに言ったのかわかりませんが、それは真実ではありません。フリー関数を使用してカプセル化を増やすことができますが、必ずしもカプセル化を増やすとは限りません。
ロブK 14

回答:


81

オブジェクトにその状態について質問し、オブジェクトの外部で行われた決定に基づいてそのオブジェクトのメソッドを呼び出すことは、オブジェクトが漏れやすい抽象化であることを意味します。その動作の一部はオブジェクトの外部にあり、内部状態は(おそらく不必要に)外部に公開されます。

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

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

もちろん、それは明らかです。私はそのようなコードを決して書かないでしょう。それでも、参照されているオブジェクトを調べて、その結果に基づいてさまざまなメソッドを呼び出すことは非常に簡単です。しかし、それが最善の方法ではないかもしれません。あなたが望むものをオブジェクトに伝えます。それを行う方法を考えてみましょう。手続き的にではなく宣言的に考えてください!

クラスの責任に基づいてクラスを設計することから始めれば、このtrapから抜け出すのは簡単です。オブジェクトの状態を通知するクエリとは対照的に、クラスが実行できるコマンドの指定に自然に進むことができます。

http://pragprog.com/articles/tell-dont-ask


4
例のテキストでは、明らかに優れた実践である多くのことを禁止しいます。
DeadMG

13
@DeadMGは、あなたがキーブックに明確に述べられているサイト名の「実用的」とサイト作者の重要な考えを盲目的に無視する、従順な人だけにあなたが言うことをします:最善の解決策はありません...」
gnat

2
本を決して読まないでください。また、私は望みません。私は完全に公平なサンプルテキストのみを読みました。
DeadMG

3
@DeadMG心配ありません。この例(およびその点についてはpragprogのその他の例)を意図したコンテキスト(「最善の解決策はありません...」)に置く重要なポイントを知ったので、本を読まなくても構いません
gnat

1
Tell、Do n't Askがコンテキストなしであなたのために何を綴るべきかはまだわかりませんが、これは本当に良いOOPアドバイスです。
エリックReppen

16

一般的に、この記事は、自分で推論できる場合は、他の人が推論するためにメンバー状態を公開すべきではないことを示唆しています

ただし、明確に述べられていないのは、推論が特定のクラスの責任を超えている場合、この法律は非常に明白な制限に該当するということです。たとえば、何らかの値を保持する、または何らかの値、特に汎用的な値を提供することを仕事とするクラス、またはクラスが拡張する必要がある動作を提供するクラス。

たとえば、システムtemperatureがクエリとしてを提供する場合、明日、クライアントはcheck_for_underheating変更せずにできますSystemMonitor。これは、自身がSystemMonitor実装する場合には当てはまりませんcheck_for_overheating。したがって、SystemMonitor温度が高すぎるときにアラームを出すことを仕事とするクラスはこれに従いますが、SystemMonitor別のコードが温度を読み取ってTurboBoostなどを制御できるようにすることを仕事とするクラス、 いけない。

また、2番目の例では、Null Object Anti-patternを無意味に使用しています。


19
「ヌルオブジェクト」はアンチパターンとは呼ばないので、そうする理由は何でしょうか?
コンラッドルドルフ

4
「何もしない」と指定されたメソッドを誰も持っていないことを確認してください。それはそれらをちょっと無意味にします。つまり、Null Objectを実装するオブジェクトは少なくともLSPを破壊し、実際にはそうではない操作を実装していると説明します。ユーザー値が戻ってくることを期待しています。彼らのプログラムの正確さはそれに依存します。価値がないのに値するふりをするだけで、より多くの問題を引き起こすことができます。静かに失敗するメソッドをデバッグしようとしたことがありますか?それは不可能であり、誰もそれを通して苦しむ必要はありません。
DeadMG

4
これは問題の領域に完全に依存していると私は主張します。
コンラッドルドルフ

5
私は、上記の例では、ヌル・オブジェクト・パターンの悪い使用であることに同意し、そこ@DeadMG あるメリットは、それを使用します。数回、いくつかのインターフェースの「no-op」実装を使用して、nullチェックを回避したり、真の「null」をシステムに浸透させたりしませんでした。
最大

6
「クライアントはcheck_for_underheating変更せずにできる」という点を確認できませんSystemMonitor。クライアントSystemMonitorはその時点とどのように違いますか?監視ロジックを複数のクラスに分散させていませんか?また、アラーム機能をそれ自体に予約しながら、センサー情報を他のクラスに提供するモニタークラスに問題はありません。ブーストコントローラーは、温度が高すぎる場合にアラームを上げることを心配せずにブーストを制御する必要があります。
TMN

9

過熱の例の本当の問題は、過熱とみなされるルールがシステムごとに簡単に変更できないことです。システムAは現在の状態(temp> 100は過熱状態)で、システムBはより繊細(temp> 93は過熱状態)であるとします。制御機能を変更してシステムのタイプを確認し、正しい値を適用しますか?

if (system is a System_A and system_monitor.temp >100)
  system_monitor.sound_alarms
else if (system is a System_B and system_monitor.temp > 93)
  system_monitor.sound_alarms
end

または、各タイプのシステムで暖房能力を定義していますか?

編集:

system.check_for_overheating

class SystemA : System
  def check_for_overheating
    if temperature > 100
      sound_alarms
    end
  end
end

class SystemB : System
  def check_for_overheating
    if temperature > 93
      sound_alarms
    end
  end
end

前者の方法では、より多くのシステムを扱うようになると、制御機能がくなります。後者は、時間が経過しても制御機能を安定させます。


1
各システムをモニターに登録しないのはなぜですか。登録時に、オーバーヒートが発生したことを示すことができます。
マーティンヨーク

@LokiAstari-できますが、湿度や大気圧にも敏感な新しいシステムに遭遇する可能性があります。原理は異なるものを抽象化うちにある-この場合には過熱への感受性である
マシュー・フリン

1
これがまさにTellモデルが必要な理由です。システムに現在の状態を伝え、それが通常の作業状態から外れているかどうかを知らせます。したがって、SystemMoniterを変更する必要はありません。それはあなたのためのカプセル化です。
マーティンヨーク

@LokiAstari-私たちはここでさまざまな目的で話していると思う-私は本当に異なるモニターではなく、異なるシステムの作成を検討していた。問題は、外部のコントローラー機能とは対照的に、システムがアラームを発生させる状態にあることを認識する必要があることです。SystemAには基準があり、SystemBには独自の基準が必要です。コントローラーは、システムが正常かどうかを(定期的に)確認できる必要があります。
マシューフリン

6

最初に、例の「悪い」と「良い」としての特徴づけについて例外をとる必要があると思います。この記事では「あまり良くない」および「より良い」という用語を使用していますが、これらの用語は選択された理由だと思います。これらはガイドラインであり、状況によっては「あまり良くない」アプローチが適切な場合があります。

選択肢が与えられた場合、外部ではなくクラスのクラスのみに依存する機能を含めることを優先する必要があります-理由はカプセル化のためであり、時間の経過とともにクラスを簡単に進化させることができるという事実です。また、このクラスは、無料機能の束よりも、その機能を宣伝するのに優れています。

決定はクラス外の何かに依存しているため、またはクラスのほとんどのユーザーに行わせたくない単純なものであるため、時々、あなたは伝える必要があります。動作はクラスにとって直観に反するため、クラスのほとんどのユーザーを混乱させたくないため、伝えたいことがあります。

たとえば、エラーメッセージを返す番地について文句を言っていますが、そうではなく、デフォルト値を提供しています。ただし、デフォルト値が適切でない場合があります。これが州または市である場合、レコードをセールスマンまたは調査担当者に割り当てるときにデフォルトを使用して、未知のすべてが特定の人に届くようにすることができます。一方、封筒を印刷する場合は、配達できない手紙に紙を無駄にしないようにする例外またはガードを好むかもしれません。

そのため、「あまり良くない」が進むべき方法である場合がありますが、一般的に、「良い」はまあ、良いです。


3

データ/オブジェクトの非対称性

他の人が指摘したように、Tell-Dont-Askは、特に尋ねた後にオブジェクトの状態変更する場合に適しています(たとえば、このページの他の場所に掲載されているPragprogテキストを参照)。これは常に当てはまるわけではありません。たとえば、「user」オブジェクトは、user.addressを求められた後に変更されません。したがって、これがTell-Dont-Askを適用する適切なケースであるかどうかは議論の余地があります。

Tell-Dont-Askは、その中に正当にあるべきクラスからロジックを引き出しないという責任に関心を持っています。しかし、オブジェクトを扱うすべてのロジックが必ずしもそれらのオブジェクトのロジックであるとは限りません。これは、Tell-Dont-Askを超えて、より深いレベルを示唆しているので、それについて簡単に述べたいと思います。

アーキテクチャ設計の問題として、実際にはプロパティのコンテナであり、不変であってもよいオブジェクトを用意し、そのようなオブジェクトのコレクションに対してさまざまな機能を実行し、コマンドを送信するのではなく、評価、フィルタリング、または変換することができますTell-Dont-Askのドメイン)。

あなたの問題により適切な決定は、安定したデータ(宣言オブジェクト)を期待するかどうかに依存しますが、関数側で変更/追加します。または、そのような関数の安定した限定されたセットを期待しているが、新しい型を追加するなど、オブジェクトレベルでより多くの流動性を期待している場合。最初の状況では、2番目のオブジェクトメソッドでは、無料の関数を好むでしょう。

ボブ・マーティンは、彼の著書「Clean Code」でこれを「データ/オブジェクト反対称性」(p.95ff)と呼んでいますが、他のコミュニティはそれを「表現の問題」と呼んでいます。


3

このパラダイムは、「伝える、尋ねない」と呼ばれることもあります。つまり、オブジェクトに何をすべきかを伝え、その状態について尋ねません。また、「Ask、do n't don '」と呼ばれることもあります。これは、オブジェクトに何かをするように頼み、その状態がどうあるべきかを伝えないことを意味します。ベストプラクティスを回避する方法はどちらも同じです。オブジェクトがアクションを実行する方法は、呼び出し元のオブジェクトではなく、オブジェクトの関心事です。インターフェイスは、その状態の公開を避け(アクセサまたはパブリックプロパティを介して)、代わりに実装が不透明な「doing」メソッドを公開する必要があります。他の人は、実用的なプログラマーへのリンクでこれをカバーしています。

このルールは、「二重ドット」または「二重矢印」のコードを回避についての規則に関連して、多くの場合、国の直接の友人にのみ話'と呼ばれるfoo->getBar()->doSomething()代わりに使用し、悪いfoo->doSomething();バーの機能のラッパーの呼び出しがあると、単純に実装されますreturn bar->doSomething();foo管理を担当する場合はbar、そうしてください!


1

「伝えるな、聞かないで」についての他の良い答えに加えて、役立つかもしれないあなたの特定の例についてのいくつかのコメント:

C ++でのアドバイスは、カプセル化を増やすため、メンバー関数ではなくフリー関数を好むことです。これらは両方とも意味的に同一なので、なぜより多くの状態にアクセスできる選択肢を好むのでしょうか?

その選択は、より多くの状態にアクセスできません。両方とも同じ量の状態を使用してジョブを実行しますが、「悪い」例では、作業を行うためにクラス状態がパブリックであることが必要です。さらに、「悪い」例でのそのクラスの振る舞いは、free関数にまで広がっており、見つけにくく、リファクタリングが難しくなっています。

無関係なエラー文字列をフォーマットするのはなぜユーザーの責任ですか?ストリートがない場合に「ファイルにストリート名がありません」と印刷する以外に何かしたい場合はどうすればよいですか?通りの名前が同じものである場合はどうなりますか?

「ストリート名の取得」と「エラーメッセージの提供」の両方を行うことが「ストリート名」の責任であるのはなぜですか?少なくとも「良い」バージョンでは、各ピースに1つの責任があります。それでも、それは素晴らしい例ではありません。


2
それは真実ではない。あなたは、過熱のチェックが温度と関係がある唯一の健全なことであると仮定します。クラスが多数の温度モニターの1つになることを意図しており、たとえば、システムがその結果の多くに応じて異なるアクションを実行する必要がある場合はどうなりますか?この動作単一インスタンスの事前定義された動作に制限できる場合は、確認してください。そうでない場合、明らかに適用できません。
DeadMG

確かに、またはサーモスタットとアラームが異なるクラスに存在する場合(おそらくそうであるように)。
テラスティン

1
@DeadMG:一般的なアドバイスは、アクセスが必要になるまで物をプライベート/保護することです。この特定の例はmehですが、それは標準的な慣行に異議を唱えるものではありません。
グヴァンテ

「meh」であることを実践することに関する記事の例は、それについて異議を唱えています。この方法が大きなメリットのために標準的なものである場合、なぜ適切な例を見つけるのに苦労するのでしょうか?
Stijn・デ・ウィット

1

これらの回答は非常に優れていますが、強調するための別の例を次に示します。これは通常、重複を避ける方法であることに注意してください。たとえば、次のようなコードを持つ複数の場所があるとします。

Product product = productMgr.get(productUuid)
if (product.userUuid != currentUser.uuid) {
    throw BlahException("This product doesn't belong to this user")
}

つまり、次のようなものを用意した方がよいということです。

Product product = productMgr.get(productUuid, currentUser)

その複製は、インターフェースのほとんどのクライアントが、あちこちで同じロジックを繰り返すのではなく、新しいメソッドを使用することを意味するためです。自分で行うために必要な情報を求めるのではなく、デリゲートに必要な作業を与えます。


0

これは、高レベルのオブジェクトを記述する場合はより真実であるが、すべてのクラスコンシューマを満足させるためにすべてのメソッドを記述することは不可能であるため、クラスライブラリなどのより深いレベルに行く場合は真実ではないと考えます。

たとえば、#2は単純化されすぎていると思います。これを実際に実装しようとすると、SystemMonitorには、同じクラスに埋め込まれた低レベルのハードウェアアクセスのコードと高レベルの抽象化のロジックが含まれることになります。残念ながら、それを2つのクラスに分割しようとすると、「教えて、聞かないで」自体に違反することになります。

例4はほぼ同じです。UIロジックをデータ層に埋め込みます。ここで、アドレスがない場合にユーザーが見たいものを修正する場合、データ層のオブジェクトを修正する必要があります。また、この同じオブジェクトを使用する2つのプロジェクトがnullアドレスに異なるテキストを使用する必要がある場合はどうなりますか?

すべてのことを「教えて、聞かないで」を実装できれば、それは非常に便利だと思います。実生活で尋ねる(そして自分でやる)のではなく、ただ伝えることができれば幸いです!ただし、実生活と同じように、ソリューションの実現可能性は高レベルのクラスに非常に限定されます。

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