- - PR -
インターフェイスによる継承と抽象化クラスによる継承の使い分け
投稿者 | 投稿内容 | ||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
投稿日時: 2007-09-03 22:31
飛べるもの----鳥----カラス
| +---飛行機----セスナ 参考までに私ならこんな風に考えます。 動物----鳥(Mix-In 飛べるもの)----カラス 乗り物---飛行機(Mix-In 飛べるもの)----セスナ C#のinterfaceとMIX-INの関係について(参考) http://www.rubyist.net/~matz/20040128.html | ||||||||||||||||||||||||||||||||
|
投稿日時: 2007-09-03 22:40
この話の流れ的に、多分、水中ユニットが出てきて、 そうなってくると水陸両用ユニットはどうすんだ〜という話になって、 地上ユニット、水中ユニットの多重継承はC#ではできないので、 やっぱりインターフェースじゃん。みたいになる?(深読みしすぎ?)
私であれば、 動物----鳥----カラス(Mix-In 飛べるもの) +---ニワトリ 乗り物---飛行機(Mix-In 飛べるもの)----セスナ とするかな?と思います。 話の腰を折ってすみません。 基本的には、設計時は未記入さんのようにやはり全てインターフェースなのかなと思います。 製造時のインターフェースの使い方については、適宜利用すればいいのではないでしょうか? (Javaで恐縮ですがSerializableインターフェースのように識別の意味に使われたりしますし…) 実際の製造段階では設計時に定義したインターフェースをimplementsすると思います。 機能の重複については、派生クラスにするかユーティリティクラスを作っておいて呼び合うか等 いろいろ方法はあると思います。(当然に、明確な機能追加であれば派生クラスにしますが…) | ||||||||||||||||||||||||||||||||
|
投稿日時: 2007-09-04 00:18
そもそも、「抽象メソッド」と「インターフェイス」を比較しようとするから混乱するのでは?
比較すべきは、「クラスによる継承」と「インターフェイス」ですよね。 「インターフェイス」のメリットは、あるクラスが属する継承のツリーとは全然関係無い別のツリーに属するクラスからも使えること。 例えば、「生物クラス」の派生クラスである「犬クラス」と、「機械クラス」の派生クラスである「ロボットクラス」に、歩数を引数として受け取り移動した距離を返す「歩く」メソッドを定義することができる。ただし、インターフェイスは中身の実装までは提供しないので、「歩く」メソッドの中身はそれぞれのクラスが実装しなければならない。 「クラスによる継承」のメリットは、具象メソッドも実装できること。もしある空間の中に「生物クラス」の派生クラス以外に「歩く」メソッドを実装するクラスが存在せず、しかも「犬クラス」と「猫クラス」の「歩く」の動作が全く同じなら、基底クラスに抽象メソッドではなく具象メソッドとして実装することで、派生クラスには何も書かなくて良くなる。 じゃあ「抽象メソッド」と「インターフェイス」の切り分けはっていうと、その空間内にそのメソッドを実装したい別の継承ツリーに属するクラスが存在するか否か。同じ「生物クラス」にぶら下がる「犬クラス」と「猫クラス」しか「鳴く」メソッドを実装するクラスが無いのなら、わざわざ「鳴く」メソッドをインターフェイスとして基底クラスである「生物クラス」とは別の場所に定義する意味はあんまりない。 そういうことではなくて? この考え方を最初の例に適用すると。
この時点であああといいいは同じ基底クラスを継承するわけですね。仮にぼぼぼとしましょう。
C()をぼぼぼを継承しない全然別のクラスでも使いたいならインターフェイスにするでしょう。 そして、あああといいいが両方C()を実装するなら、ぼぼぼにそのインターフェイスをくっつけてしまうでしょう。 C()を実装するのがぼぼぼを継承するクラスだけならば、インターフェイス化する必要はないので、 ぼぼぼに直接C()を実装するでしょう。
この場合は、あああとうううに共通する処理をぼぼぼに実装して、いいいだけオーバーライドです。 いいいがもし「全く別の処理」ではなく「他と同じ処理+α」なら メソッド内で基底クラスの同メソッドを呼び出して使うことだってできますよね。
そうです。全継承先の処理内容が完全に異なり、引数と戻り値だけ同じならそれでいいし、 少しでも共通部分があるならそれを基底クラスに実装して継承先から使えるようにしてもいいですよね。
気持ち悪いですか?「このクラスにはこういう引数でこういう戻り値のメソッドがあるよん」ていう宣言ですよ。そこだけ外部に切り出してぼぼぼと全然関係無いクラスからも使えるようにしたのがインターフェイスとも言えるわけで。 この例だと、あんまり迷う余地はないです。むしろ私が迷うのは、 あああはA()、B()、C()を実装し、いいいもA()、B()、C()を実装し、うううはA()、B()、D()を実装する、という状態。 これがはじめっからわかっているなら、
こういう継承ツリーを作っちゃえばいいんですが。
問題は既にこういう実装をしちゃった後にA()、B()、D()を実装するうううが現れた場合です。 数の上では少数派のうううのために、上位のクラスをわざわざ作り変えるか?もしくはうううはツリーにぶら下げず独立した別のクラスにするか?それとも、うううをそのままぶら下げてC()だけ見えないように隠蔽しちゃうか?はたまた、隠蔽すらせずに普通に使えるんだけど、呼ばれてもなんにもしないようにするのか? 要は、「鳥は飛ぶもの」という前提で実装完了した後に、次バージョンでニワトリが現れた場合ですね。 #実はニワトリも飛ぶような動きはするんですよね。大空高く舞い上がったりはしないけど。 | ||||||||||||||||||||||||||||||||
|
投稿日時: 2007-09-04 12:17
継承ツリーでは、その問題領域で根本的で不変と見られるカテゴリーやグループ分けを表すのがいいと思います。 継承を使うものは必ず「is a」ですが、「is a」だから必ず継承を使わなければいけないわけではありません。 未記入(ID:202837)さんの「言語仕様を意識しない設計レベル」というのは問題領域の概念モデリング的な意味合いだと思いますが、そこでは実装は意識しませんので
とふたつの継承関係を考えていいと思います。UMLで書けば汎化セットを使うことになるでしょうか。 で、実装を考えた設計をする段になると、地上/飛行というのを継承で表現するという選択肢は設計の初期の段階で除外しますね。私が設計するなら。 基本的には「地上ユニット」も「飛行ユニット」も外部からは「ユニット」として扱うのだと思います。例えば移動出来る範囲を取得するときに、その計算方法などは異なりますが外部からは単に「移動範囲を取得」します。 それ以外に飛行ユニット独自の処理もあると思います。例えばターン終了時に燃料(や体力)切れによる墜落判定をするというのは飛行ユニット独自の処理です。 しかしこれらを表現するには継承は大げさで重すぎると思います。(出てきている話だけから想像するに) 私なら飛行か地上かは列挙型のプロパティで表し、移動範囲を取得処理などの中でif文で処理を分けるかStrategyパターン?的な感じにします。 飛行ユニット独自の墜落判定も、自分が地上ユニットの場合は常に墜落しないことにするだけです。わざわざ地上ユニットには墜落判定の機能を持たせないために継承を使ったりしません。そうすると、 カタパルト 能力:「隣接する味方ユニット一体を次のターンの終了時まで飛行ユニットにする。対象となったユニットが飛行ユニットでなくなった場合、場から取り除く。」 なんて能力が出てきても安心です。継承を使っているとこうはいきませんよね。ですので、
多分私ならインターフェイスにもしません。 大型/小型も、継承にもインターフェイスにもならないですね。 これは論理的に「こうだからこうするのが正解」と説明するのは難しいです。私のゲーム作りの経験から、おそらくこうすると最もうまく行くんじゃないだろうかと感じているだけです。 なので、「こう考えるヤツもいる」程度に受け止めてください。 継承というのは結構重厚な効果をもたらします。それが良いこともありますし、足かせになってしまうこともあります。 if文を減らすためとか、同じ機能があるからということで使うのはやめたいですね。
いつ私が未記入さんを陥れる悪の組織のメンバーになったんですか? よってたかって攻撃されているみたいな感覚を感じてらっしゃるんでしょうか。 | ||||||||||||||||||||||||||||||||
|
投稿日時: 2007-09-04 12:52
ゲームなんて作ったことのない私が、変なゲームのたとえを出したのが悪かったのでしょうね。謝ります。
なるほど。つまり、あらゆるプログラム変更・拡張の可能性を考慮すると、そもそも多態性自体が邪魔になるという主張なんですね? それでは、あとはお好きにしてください。 | ||||||||||||||||||||||||||||||||
|
投稿日時: 2007-09-04 20:04
当初の質問である使用基準について考えてみました。
.NET(CLR)のコーディングにおいて、抽象クラス(仕様公開+実装)より制約の少ないインタフェース(仕様公開)を抽象クラスの代わりに使うことは可能で、汎化(is-a)の関係をインタフェースを利用して実装できます。このため、is-a関係かどうかをもとに抽象クラスとインタフェースの使い分けは判断できないとする点は理解できます。 じゃ判断基準をどのようにするかですが、実装としてコードがどれぐらい再利用できるかや、抽象クラスを利用しないで全てインタフェースで代用するような選択肢は確かにあるのですが、インタフェースの多用による凝集度の低下(同じコードが複数現れる)や、 誤った抽象クラスの利用で実装コードが不必要に依存することが予想されます。要はコードがたまたま同じなのか本質的に再利用可能なのかの判断をどのようにするかを考える必要があるのではないでしょうか? この判断基準を、実装するインタフェースや抽象クラスがどのような意味付けのものかに求めるのは悪い選択ではないと考えています。今までもいくつかの指針がでているのでそれを基準に使う方法もあります。より具体的にアイデアとしては、クラスまたはインターフェースを継承する際の意味付けを汎化・MIX-IN・仕様公開に分類して実装方法を決定する方法もあるのではないかなと思っています。 1.汎化の関係(バリエーションの追加) −> 抽象クラス(多重継承時はインタフェース) 2.MIX-IN(機能の追加) −> インタフェース 3.仕様公開・インタフェースの公開(契約の追加) −> インタフェース 1,2は実装コードを再利用しますが、3は実装コードの再利用を目的としません。1と2の違いは、サブタイプをバリエーションとして定義するか、単なる機能を追加するのかの違いです。 この方法が完璧だというわけではありませんが、仕様公開を目的にしている用途で抽象クラスを使ってしまうようなことは防ぐことは可能です。 追記 objectクラスに対するMIX-INを抽象クラスで実装することは可能ですね。C#がMIX-INを直接サポートしてくれればこんな悩みはないのですが...次の3.0であれば拡張メソッドの利用も選択肢になりますね。 [ メッセージ編集済み 編集者: dotnetmemo 編集日時 2007-09-04 22:00 ] | ||||||||||||||||||||||||||||||||
|
投稿日時: 2007-09-04 23:47
当初の質問とは直接関係の無い話かもしれないので、
ひょっとすると別スレの方がいいのかもしれませんが、 継承・汎化のあたり全く関連性の無い話ではないのでここに書いちゃいますけど、 先のニワトリ問題みたいのは皆さんどのように対処されてるのでしょう? もうちょっと具体的な例をあげると、DBのテーブルを参照・更新するアプリがあるとして。 まぁ一般的な、検索条件入力画面⇔一覧画面⇔詳細画面の3画面構成で、 一覧画面からは削除ができて詳細画面からは更新と新規レコード作成ができる、 みたいなパターンです。 で、操作対象のテーブルはいくつかあり、テーブル毎に3画面のペアがあるものとします。 で、それをMVCで作った場合のModelの部分。 各テーブルの項目や検索条件は当然異なるにしろ、操作内容がほぼ一緒なら、 検索()、更新()、削除()、新規()の4つのメソッドを定義した基底クラス(もしくはインターフェイス) から派生させて各テーブルに合わせたクラスを作るってのは結構一般的かと思うのですが。 例えばあるテーブルに関しては「削除()と新規()は必要ない」という場合、 メソッド自体は継承しちゃって、中身としては何もしないもしくはNotSupportedException、 というのがいいのか。 もしくはこの設計は間違っていて、そういう要件を想定してあるクラスが検索()と更新()のみ実装、 みたいなことができるようにする方がいいのか。 で、もしそうだとしたらそれを実現できるのは具体的にどういう設計なのか。 スレ主さんの求めるものとは少し質の異なる問題なのかもしれませんが、 できればこちらに関しても皆さんのご意見を伺ってみたいとこです。 | ||||||||||||||||||||||||||||||||
|
投稿日時: 2007-09-05 05:42
こんな感じですか? ・全部インターフェース
・継承+IsXXXableプロパティ
とりあえず2パターン出しましたが、もっとあるでしょうね。 私ならどうするかとか、 私は「こうするべき」と思ってるとか、 そういったことしか答えられませんが。 DBのテーブルのラッパの場合、 私なら後者で作ります。 普段は追加不可能でも、動的に追加可能になったりする場合に対応できますし。 パフォーマンスも一番いいですし、 オブジェクト数が少なくて済みますし、 操作が可能かどうかはIsXxxableを見ればいいだけですから。 前者はある操作の可能・不可能を動的に変えられません。 メソッドもプロパティも継承時に全部書かないといけません。 オブジェクトを一々IXxxableにキャストするのもめんどくさいです。 メソッドの引数などで、「IXxxableでIYyyableなもの」という制約が欲しい場合、 IZzzableを作らないとだめで、 そうやっていくとインターフェース数が増え続けてしまいます。 #System.IO.StreamもIsReadable、IsWritableですね じゃあ一般にどういうものならIXxxableを使って、 どういうものなら使わないのかと言われると…、すごく困ります。 私には答えられません。 動的に変わる可能性があるというのは考慮してますね。たぶん。 「制限の組み合わせ」を課す可能性のある場合は IsXxxableなコードを使ってるような気もします。 IReadableAndWritableButNotSearchableなんてInterfaceは作ってませんね。たぶん。 ですから、「〜できる」場合はInterface、とは判断してません。 Is-aの時は継承、とも判断してません。 最終的にできるであろうコードの質と量、実装の手間、 そういったものを予想して決定しているんでしょうね。 悩んだ場合は ・偉い人のコードをみる ・とりあえず両方作ってみる で解決してます。 #「使用基準は経験と勘」では誰の役にも立たない… #すみません。 |