多型のコンテキストでサブタイプに追加されたメソッドを処理する方法は?


14

ポリモーフィズムの概念を使用してクラス階層を作成し、親の参照を使用すると、オブジェクトを持っている特定の型を知らなくてもインターフェイス関数を呼び出します。これは素晴らしい。例:

動物のコレクションがあり、すべての動物の機能を呼び出しeatます。犬を食べるのか、猫を食べるのかは気にしません。継承と実装クラス以外から-しかし、同じクラス階層には、追加を持っている動物持ってAnimal、例えばmakeEggsgetBackFromTheFreezedStateようにしています。そのため、関数の一部のケースでは、追加の動作を呼び出す特定のタイプを知りたい場合があります。

たとえば、朝の時間で、動物だけの場合はを呼び出しますeat。それ以外の場合は、人間の場合は最初washHandsに呼び出し、次に呼び出しgetDressedますeat。この場合の対処方法は?多型が死ぬ。コードの匂いのように聞こえるオブジェクトのタイプを調べる必要があります。このケースを処理する一般的なアプローチはありますか?


7
説明したポリモーフィズムのタイプは、サブタイピングポリモーフィズムと呼ばれますが、それだけではありません(ポリモーフィズムを参照)。ポリモーフィズムを実行するためにクラス階層を作成する必要はありません(実際、継承はサブタイプポリモーフィズムを達成するための最も一般的な方法ではなく、インターフェイスの実装がはるかに普及していると主張します)。
ヴィンセント

24
あなたが定義した場合Eaterとのインタフェースeat()方法を、そしてクライアントとして、あなたはそれを気にしないHuman最初の呼び出しにそれが持っているの実装washHands()getDressed()、それはこのクラスの実装の詳細です。クライアントとして、あなたがこの事実に関心があるなら、あなたはその仕事に正しいツールを使用していない可能性が高いでしょう。
ビンセントサ

3
また、午前中は、人間がgetDressed彼らの前に必要になるかもしれないことを考慮する必要がありますがeat、それは昼食には当てはまりません。状況によっては、washHands();if !dressed then getDressed();[code to actually eat]これを人間に実装する最良の方法かもしれません。別の可能性は、他のものがそれwashHandsを必要とする場合、および/またはgetDressed呼び出される場合はどうなりますか?あなたが持っていると仮定しますleaveForWorkか?とにかくずっと前に呼び出されるように、プログラムのフローを構造化する必要があるかもしれません。
ダンカンXシンプソン

1
正確な型に対するチェックはOOPではコードの匂いかもしれませんが、FPでは非常に一般的な慣習です(つまり、パターンマッチングを使用して、識別されたユニオンの型を決定し、それに基づいて動作します)。
セオドロスチャツィジアンナキス

3
動物のようなオブジェクト指向階層の学校部屋の例に注意してください。実際のプログラムには、このようなクリーンな分類法はほとんどありません。例えば、ericlippert.com/2015/04/27/wizards-and-warriors-part-one。または、すべてを独り占めして、パラダイム全体に疑問を投げかけたい場合は、オブジェクト指向プログラミングは悪いです。
jpmc26

回答:


18

依存します。残念ながら、一般的な解決策はありません。要件を検討し、これらの処理が何をすべきかを理解してください。

たとえば、あなたは午前中にさまざまな動物がさまざまなことをするという。方法getUp()prepareForDay()そのようなものを紹介してください。その後、多型を続行し、各動物に朝のルーチンを実行させることができます。

動物を区別したい場合は、無差別にリストに保存しないでください。

他に何も機能しない場合Visitor Patternを試してみてください。これは、動物からの正確なコールバックを受信する訪問者を送信できる、一種の動的ディスパッチを可能にするハックのようなものです。しかし、他のすべてが失敗した場合、これは最後の手段であるべきだと強調します。


33

これは良い質問であり、OOの使用方法を理解しようとするとき、多くの人にとっては一種の問題です。ほとんどの開発者はこれに苦労していると思います。私はほとんどがそれを乗り越えると言うことができればいいのですが、私はそうではないのです。私の経験では、ほとんどの開発者は擬似OOプロパティバッグを使用することになります。

まず、はっきりさせてください。これはあなたのせいではありません。OOが通常教えられる方法は非常に欠陥があります。Animal例では、IMO、最高の犯罪者です。基本的に、私たちは言う、オブジェクトについて、それらが何ができるかについて話しましょう。Animalことができますeat()し、それができspeak()。素晴らしい。次に、いくつかの動物を作成し、それらがどのように食べ、話すかをコード化します。今、あなたはオブジェクト指向を知っていますよね?

問題は、これがオブジェクト指向で間違った方向から来ていることです。このプログラムに動物がいるのはなぜですか、なぜ彼らは話したり食べたりする必要があるのですか?

Animal型の実際の使用を考えるのに苦労しています。確かに存在しますが、推論するのが簡単だと思うもの、つまり交通シミュレーションについて議論しましょう。さまざまなシナリオでトラフィックをモデル化するとします。これを実現するために必要な基本的な事項を次に示します。

Vehicle
Road
Signal

あらゆる種類の歩行者や電車をより深く掘り下げることができますが、シンプルに保ちます。

考えてみましょうVehicle。車両にはどのような能力が必要ですか?道路を走行する必要があります。信号で停止できる必要があります。交差点をナビゲートできる必要があります。

interface Vehicle {
  move(Road road);
  navigate(Road... intersection);
}

これはおそらく簡単すぎるかもしれませんが、始まりです。今。車両が行う可能性のある他のすべてのことはどうですか?彼らは道路から離れて溝に曲がることができます。シミュレーションの一部ですか?いいえ。必要ありません。一部の車とバスには、それぞれバウンスまたはひざまずくことができる油圧機構があります。シミュレーションの一部ですか?いいえ。必要ありません。ほとんどの車はガソリンを燃やします。しない人もいます。発電所はシミュレーションの一部ですか?いいえ。必要ありません。車輪の大きさ?必要ありません。GPSナビゲーション?インフォテインメントシステム?emは必要ありません。

使用する動作のみを定義する必要があります。そのためには、オブジェクト指向インターフェイスと相互作用するコードからオブジェクト指向インターフェイスを構築する方が良いと思います。空のインターフェイスから始めて、存在しないメソッドを呼び出すコードの記述を開始します。これが、インターフェースで必要なメソッドを知る方法です。それができたら、これらの動作を実装するクラスの定義を開始します。使用されない動作は無関係であり、定義する必要はありません。

OOの全体的なポイントは、後で呼び出しコードを変更せずにこれらのインターフェイスの新しい実装を追加できることです。動作する唯一の方法は、呼び出し元のコードのニーズがインターフェイスで何を行うかを決定する場合です。後で考えられる可能性のあるすべてのもののすべての動作を定義する方法はありません。


13
これは良い答えです。「そのためには、オブジェクト指向インターフェイスと相互作用するコードからオブジェクト指向インターフェイスを構築する方が良いと思います。」絶対に、そして私はそれが唯一の方法だと主張します。実装だけではインターフェイスのパブリックコントラクトを知ることはできず、常にクライアントの観点から定義されます。(補足として、これがTDDの実際の
目的

@VincentSavard「それが唯一の方法だと思う」あなたが正しいです。私が絶対にそれをしなかった理由は、いったんアイデアが得られたら、インターフェイスを少し肉付けしてから、このように洗練できるからだと思います。最終的には、真ちゅうのタックにたどり着くとき、それだけが重要です。
ジミージェームズ

@ jpmc26おそらく、少し強く表現されています。これを実装することはめったにないことに同意します。ひどいアイデアだと思うマーカーインターフェイスを別にして、この方法でインターフェイスを使用していない場合、インターフェイスがどのように役立つかはわかりません。
ジミージェームズ

9

TL; DR:

すべてのサブクラスに適用され、必要なすべてをカバーする抽象化とメソッドについて考えてください。

最初にeat()例を続けましょう。

食べることの前提条件として、食べる前に手を洗って服を着たいと思うのは、人間であることの特性です。あなたはあなたと一緒に朝食を食べるために、誰かがあなたに来てほしい場合は、していない教えてあなたがそれらを招待するとき、彼らは自分自身でそれを行うに手を洗うようにして服を着るそれらを、または彼らは、私が来ることができない」と答えます以上、私は手を洗っておらず、まだ服を着ていません」。

ソフトウェアに戻る:

以下のようHumanな前提条件なしで食べられないだろう例えば、私が持っていると思いHumanさんeat()行う方法をwashHands()し、getDressed()それが行われていない場合。eat()その特異性を知ることは、発信者としてのあなたの仕事であってはなりません。頑固な人間の代替策は、前提条件が満たされていない場合に例外をスローすることです(「私は食べる準備ができていません!」)。

どうmakeEggs()

あなたの考え方を変えることをお勧めします。おそらく、すべての存在の予定された朝の任務を実行したいでしょう。繰り返しますが、呼び出し元として、彼らの義務が何であるかを知ることはあなたの仕事であってはなりません。したがってdoMorningDuties()、すべてのクラスが実装するメソッドをお勧めします。


私はこの答えに同意します。Narekはコードの匂いについて正しいです。悪臭を放つのはインターフェイス設計なので、それを修正し、あなたの利益を高めます。
ジョナサン

この答えが説明することは通常Liskov Substitution Principleと呼ばれます。
フィリップ

2

答えは非常に簡単です。

予想以上のことができるオブジェクトをどのように処理しますか?

目的を果たさないため、処理する必要はありません。インターフェイスは通常、使用方法に応じて設計されます。インターフェースが手を洗うことを定義していない場合、インターフェースの呼び出し元としてそれを気にしません。あなたがそうすれば、あなたはそれを異なって設計したでしょう。

たとえば、朝の時間で動物だけの場合はeatを呼び出し、そうでない場合は人間の場合は最初にwashHands、getDressedを呼び出してからeatを呼び出します。この場合の対処方法は?

たとえば、擬似コードでは:

interface IEater { void Eat(); }
interface IMorningRoutinePerformer { void DoMorningRoutine(); }
interface IAnimal : IEater, IMorningPerformer;
interface IHuman : IEater, IMorningPerformer; 
{
  void WashHands();
  void GetDressed();
}

void MorningTime()
{
   IList<IMorningRoutinePerformer> items = Service.GetMorningPerformers();
   foreach(item in items) { item.DoMorningRoutine(); }
}

これで、食事IMorningPerformerAnimalするためだけにHuman実装し、手を洗って服を着るためにも実装します。MorningTimeメソッドの呼び出し元は、それが人間でも動物でもかまいません。必要なのは、実行される朝のルーチンだけです。各オブジェクトは、オブジェクト指向のおかげで見事に実行されます。

多型は死にます。

それともそうですか?

オブジェクトのタイプを調べる必要があります

なぜそうなっているのですか?これは間違った仮定かもしれません。

このケースを処理する一般的なアプローチはありますか?

はい、通常は、慎重に設計されたクラスまたはインターフェース階層で解決されます。上記の例では、あなたが与えた例と矛盾するものはありませんが、不満を感じるでしょう。 、これらの仮定はおそらく違反されています。

あなたがあなたの仮定を厳しくすることでウサギの穴に行くことは可能です、そして、私はそれらをまだ満たすために答えを修正します、しかし、私はそれが役に立つだろうとは思いません。

適切なクラス階層を設計することは難しく、ビジネスドメインに対する多くの洞察が必要です。複雑なドメインでは、適切なモデルに到達するまで、ビジネスドメイン内のさまざまなエンティティがどのように相互作用するかについての理解を洗練するため、2、3、またはそれ以上の反復を繰り返します。

それは単純な動物の例が欠けているところです。私たちは簡単に教えたいのですが、解決しようとしている問題は、あなたがより深くなるまで、つまり、より複雑な考慮事項と領域を持つまで明らかではありません。


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