上記の質問は、私がレガシーコードで遭遇する一般的な問題、より正確には、この問題を解決するための以前の試みから生じる問題の抽象的な例です。
Enumerable.OfType<T>
メソッドのように、この問題に対処することを目的とした.NETフレームワークメソッドを少なくとも1つ考えることができます。しかし、最終的に実行時にオブジェクトのタイプを問い合わせることになるという事実は、私には適切ではありません。
各馬に「ユニコーンですか」と尋ねるだけでなく、次のアプローチも思い浮かびます:
これは「無回答」で最もよく答えられると感じています。しかし、あなたはこの問題にどのように取り組みますか、そしてそれが依存する場合、あなたの決断の前後関係は何ですか?
この問題が関数型コードにまだ存在するかどうか(または、可変性をサポートする関数型言語にのみ存在するかどうか)についての洞察にも興味がありますか?
これは、次の質問の重複の可能性があるとしてフラグが付けられました: ダウンキャストを回避する方法?
その質問への答えは、すべてのホーン測定を行う必要があるHornMeasurer
を所持していることを前提としています。しかしそれは、誰もが馬の角を自由に測定できるという平等主義の原則に基づいて形成されたコードベースに対するかなりの義務です。
HornMeasurer
がない場合、受け入れられた回答のアプローチは、上記の例外ベースのアプローチを反映しています。
また、馬とユニコーンの両方がウマであるかどうか、またはユニコーンが馬の魔法の亜種であるかどうかについてのコメントにも混乱がありました。両方の可能性を検討する必要があります-おそらく一方が他方より好ましいのでしょうか?
Unicorn
をHorse
の特別な種類として扱いたいと仮定すると、それをモデル化する方法は基本的に2つあります。より伝統的な方法はサブクラスの関係です。コードをリファクタリングするだけでタイプのチェックとダウンキャストを回避でき、常に重要なコンテキストでリストを分離し、Unicorn
トレイトを気にしないコンテキストでのみリストを組み合わせます。言い換えると、そもそも馬の群れからユニコーンを抽出する必要があるような状況に陥らないように配置します。これは最初は難しいように見えますが、99.99%のケースで可能であり、通常は実際にコードをよりクリーンにします。
ユニコーンをモデル化するもう1つの方法は、すべての馬にオプションの角の長さを与えることです。次に、角の長さがあるかどうかを確認してユニコーンかどうかをテストし、(Scalaで)すべてのユニコーンの平均角の長さを調べます。
case class Horse(val hornLength: Option[Double])
val horse = Horse(None)
val Unicorn = Horse(Some(12.0))
val anotherUnicorn = Horse(Some(6.0))
val herd = List(horse, Unicorn, anotherUnicorn)
val hornLengths = herd flatMap {_.hornLength}
val averageLength = hornLengths.sum / hornLengths.size
この方法には、単一のクラスを使用する場合、より単純であるという利点がありますが、拡張性がはるかに低く、「ユニコーン性」をチェックするための迂回的な方法があるという欠点があります。このソリューションを使用する場合の秘訣は、より柔軟なアーキテクチャに移行する必要があることを頻繁に拡張し始める時期を認識することです。この種類のソリューションは、flatMap
のようなシンプルで強力な関数を使用してNone
アイテムを簡単にフィルターで除外する関数型言語で非常に人気があります。
あなたはほとんどすべてのオプションをカバーしました。特定のサブタイプに依存する動作があり、それが他のタイプと混在している場合、コードはそのサブタイプを認識する必要があります。それは単純な論理的推論です。
個人的には、horses.OfType<Unicorn>().Average(u => u.HornLength)
を使用します。それはコードの意図を非常に明確に表現します。誰かが後でそれを維持しなければならないことになるので、これはしばしば最も重要なことです。
.NETには何も問題はありません:
_var Unicorn = animal as Unicorn;
if(Unicorn != null)
{
sum += Unicorn.HornLength;
count++;
}
_
Linqと同等のものを使用することも問題ありません。
_var averageUnicornHornLength = animals
.OfType<Unicorn>()
.Select(x => x.HornLength)
.Average();
_
タイトルで尋ねた質問に基づいて、これは私が見つけることを期待するコードです。 「角のある動物の平均は何ですか」のような質問の場合、それは異なるでしょう。
_var averageHornedAnimalHornLength = animals
.OfType<IHornedAnimal>()
.Select(x => x.HornLength)
.Average();
_
Linqを使用する場合、列挙子が空でタイプTがnullにできない場合、Average
(およびMin
およびMax
)は例外をスローすることに注意してください。これは、平均が実際には定義されていないためです(0/0)。したがって、実際には次のようなものが必要です。
_var hornedAnimals = animals
.OfType<IHornedAnimal>()
.ToList();
if(hornedAnimals.Count > 0)
{
var averageHornLengthOfHornedAnimals = hornedAnimals
.Average(x => x.HornLength);
}
else
{
// deal with it in your own way...
}
_
編集
これを追加する必要があると思います...このような質問がオブジェクト指向のプログラマーにうまく合わない理由の1つは、データ構造をモデル化するためにクラスとオブジェクトを使用していると想定していることです。元のSmalltalk風のオブジェクト指向のアイデアは、オブジェクトとしてインスタンス化され、メッセージを送信したときにサービスを実行するモジュールからプログラムを構成することでした。クラスとオブジェクトを使用してデータ構造をモデル化できるという事実は(便利な)副作用ですが、これらは2つの異なるものです。 couldがstruct
を使って同じことをするので、後者はオブジェクト指向プログラミングと考えるべきではないと思いますが、それほど美しくはありません。
オブジェクト指向プログラミングを使用して何かを行うサービスを作成している場合、そのサービスが実際に他のサービスであるのか、それとも具体的な実装であるのかを問い合わせることは、通常、正当な理由で不快になります。インターフェイスが(通常は依存関係の注入によって)与えられ、そのインターフェイス/契約にコーディングする必要があります。
一方、データ構造またはデータモデルを作成するためにクラス/オブジェクト/インターフェイスのアイデアを(誤って)使用している場合、私は個人的にis-aのアイデアを最大限に使用することに問題はないと思います。ユニコーンが馬のサブタイプであり、ドメイン内で完全に理にかなっていると定義した場合は、絶対に先に進み、群れの馬にクエリを実行してユニコーンを見つけます。結局のところ、このような場合、私たちは通常、ドメイン固有の言語を作成して、解決しなければならない問題の解決策をよりよく表現しようとしています。その意味で、.OfType<Unicorn>()
などに問題はありません。
結局、アイテムのコレクションを取り、それをタイプでフィルタリングすることは、実際には単なる関数型プログラミングであり、オブジェクト指向プログラミングではありません。ありがたいことに、C#のような言語は現在、両方のパラダイムを快適に処理しています。
しかし、最終的に実行時にオブジェクトのタイプを問い合わせることになるという事実は、私には適切ではありません。
このステートメントの問題は、使用するメカニズムに関係なく、常にオブジェクトに問い合わせて、そのタイプを通知することです。これはRTTIの場合もあれば、共用体または単純なデータ構造の場合もあり、if horn > 0
。正確な詳細は少し異なりますが、意図は同じです。オブジェクトに何らかの形で問い合わせて、さらに調べる必要があるかどうかを確認します。
そのため、これを行うには、言語のサポートを使用するのが理にかなっています。 .NETでは、たとえばtypeof
を使用します。
これを行う理由は、単に言語を上手に使うことだけではありません。別のオブジェクトのように見えるが、少しの変更ではあるオブジェクトがある場合、時間の経過とともにさらに多くの違いが見つかる可能性があります。ユニコーン/馬の例では、角の長さがあると言うかもしれませんが、明日は、潜在的なライダーが処女かどうか、またはうんちがきらきらしているかどうかを確認します。 (実際の古典的な例は、共通のベースから派生するGUIウィジェットであり、チェックボックスとリストボックスを別々に探す必要があります。データのすべての可能な置換を保持する単一のスーパーオブジェクトを単純に作成するには、違いの数が多すぎます。 )。
実行時にオブジェクトのタイプをチェックしてもうまくいかない場合は、別の方法として、最初から異なるオブジェクトを分割します。ユニコーン/馬の単一の群れを保存する代わりに、2つのコレクションを保持します。1つは馬用、もう1つはユニコーン用です。 。これは、特殊なコンテナー(たとえば、キーがオブジェクトタイプであるマルチマップ)にそれらを格納する場合でも非常にうまく機能しますが、2つのグループに格納しても、オブジェクトタイプの調査にすぐに戻ることができます。 !)
確かに例外ベースのアプローチは間違っています。例外を通常のプログラムフローとして使用すると、コードのにおいがします(ユニコーンの群れと頭に貝殻がテープで留められたロバがしまった場合、例外ベースのアプローチは問題ありませんが、ユニコーンの群れがある場合そして、馬がユニコーンかどうかをそれぞれチェックすることは予想外ではありません。例外は例外的な状況であり、複雑なif
ステートメントではありません)。いずれにせよ、この問題に例外を使用することは、実行時にオブジェクトタイプに問い合わせるだけであり、ここでのみ、言語機能を誤用して非ユニコーンオブジェクトをチェックしています。 if horn > 0
少なくともコレクションを迅速かつ明確に処理し、少ないコード行を使用して、他の例外がスローされたときに発生する問題を回避します(たとえば、空のコレクション、またはそのロバの貝殻を測定しようとします)。
質問にはfunctional-programming
タグでは、合計タイプを使用して馬の2つのフレーバーを反映し、パターンマッチングを使用してそれらを明確化できます。たとえば、F#では:
type Equine =
| Horse
| Unicorn of hornLength: float
module equines =
let averageHornLength (equines : Equine list) =
equines
|> List.choose (fun x ->
match x with
| Unicorn u -> Some(u)
| _ -> None)
|> List.average
let herd = [ Horse ; Horse ; Unicorn(35.0) ; Horse ; Unicorn(50.0) ]
printfn "Average horn length in herd : %f" (equines.averageHornLength herd) // prints 42.5
OOPよりも、FPには、データ/関数の分離という利点があります。これにより、リストから特定のサブタイプにダウンキャストするときに、抽象化のレベルに違反する(正当化されない?)「有罪の良心」からあなたを救うことができます。スーパータイプのオブジェクトの。
他の回答で提案されているOOソリューションとは対照的に、パターンマッチングは、Equine
の別のHorned種が1日現れる場合に、より簡単な拡張ポイントも提供します。
最後に同じ答えの短い形式では、本またはWeb記事を読む必要があります。
訪問者パターン
問題は馬とユニコーンの混合です。 (Liskov置換の原則に違反することは、従来のコードベースでは一般的な問題です。)
Horseとすべてのサブクラスにメソッドを追加する
Horse.visit(EquineVisitor v)
馬の訪問者インターフェイスは、Java/c#では次のようになります。
interface EquineVisitor {
void visitHorse(Horse z);
void visitUnicorn(Unicorn z);
}
Unicorn.visit(EquineVisitor v){
v.visitUnicorn(this);
}
Horse.visit(EquineVisitor v){
v.visitHorse(this);
}
ホーンを測定するために、次のように記述します。
class HornMeasurer implements EquineVistor {
void visitHorse(Horse h){} // ignore horses
void visitUnicorn(Unicorn u){
double len = u.getHornLength();
totalLength+=len;
unicornCount++;
}
double getAverageLength(){
return totalLength/unicornCount;
}
double totalLength=0;
int unicornCount=0;
}
ビジターパターンは、リファクタリングと成長を困難にすることで批判されています。
短い回答:デザインパターンVisitorを使用して二重ディスパッチを取得します。
参照してください https://en.wikipedia.org/wiki/Visitor_pattern
訪問者の議論については http://c2.com/cgi/wiki?VisitorPattern も参照してください。
ガンマらによるデザインパターンも参照してください。
あなたのアーキテクチャでユニコーンが馬の亜種であり、Horse
のコレクションを取得する場所でそれらの一部がUnicorn
sである可能性があると仮定すると、私は個人的に最初の方法(.OfType<Unicorn>()...
)それはあなたの意図を表現する最も簡単な方法だからです。後から来る人(3か月後の自分も含む)にとって、そのコードで何を達成しようとしているのかはすぐにわかります。馬の中からユニコーンを選びます。
あなたがリストした他の方法は、「あなたはユニコーンですか?」たとえば、ホーンを測定する何らかの例外ベースの方法を使用する場合、次のようなコードが存在する可能性があります。
foreach (var horse in horses)
{
try
{
var length = horse.MeasureHorn();
//...
}
catch (NoHornException e)
{
continue;
}
}
そのため、例外は何かがユニコーンではないという指標になります。そして、これは実際にはexceptionalの状況ではなくなりましたが、通常のプログラムフローの一部です。そして、if
の代わりに例外を使用することは、型チェックを行うよりもさらに汚いようです。
あなたが馬の角をチェックするための魔法の価値のあるルートに行くとしましょう。したがって、クラスは次のようになります。
class Horse
{
public double MeasureHorn() { return -1; }
//...
}
class Unicorn : Horse
{
public override double MeasureHorn { return _hornLength; }
//...
}
これでHorse
クラスはUnicorn
クラスについて知っている必要があり、気にしないことを処理するための追加のメソッドを持っている必要があります。ここで、Pegasus
から継承するZebra
sおよびHorse
sもあるとします。ここでHorse
にはFly
メソッドとMeasureWings
、CountStripes
などが必要です。そして、Unicorn
クラスもこれらのメソッドを取得します。これで、クラスはすべてお互いについて知る必要があり、型システムに「これはユニコーンですか?」と尋ねるのを避けるためだけにあるべきではないメソッドの束でクラスを汚染しました。
では、Horse
に何かを追加して、何かがUnicorn
であり、すべてのホーン測定を処理する場合はどうでしょうか?さて、何かがユニコーンであるかどうかを確認するために、このオブジェクトの存在を確認する必要があります(これは、あるチェックを別のチェックに置き換えるだけです)。また、実際にはすべてのユニコーンを保持するList<Horse> unicorns
が存在する可能性があるため、水を少し濁らせますが、型システムとデバッガーは簡単にはそれを知ることができません。 「でも私はそれがすべてユニコーンであることを知っています」とあなたは言いますさて、もし何かが不十分に命名された場合はどうなりますか?あるいは、本当にすべてがユニコーンであるという前提で何かを書いたが、要件が変更され、ペガシが混入した可能性があるとしたら? (これまでのように、特にレガシーソフトウェア/皮肉では何も起こらないため。)これで、タイプシステムは喜んであなたのペガシをユニコーンに入れます。変数がList<Unicorn>
として宣言されている場合、ペガシまたは馬を混ぜ合わせようとすると、コンパイラー(またはランタイム環境)が適合します。
最後に、これらのメソッドはすべて、型システムチェックの代わりにすぎません。個人的に、私はここでホイールを再発明せず、私のコードが組み込みで他の何千ものコーダーによって何千回もテストされたものと同じように機能することを願っています。
結局のところ、コードはyoが理解できる必要があります。コンピューターは、どのように書いてもそれを理解します。あなたはそれをデバッグし、それについて推論することができる人です。あなたの仕事をより簡単にする選択をしてください。何らかの理由で、これらの他の方法の1つが、表示されるいくつかの箇所でより明確なコードよりも優れているという利点がある場合は、それを試してください。しかし、それはあなたのコードベースに依存します。
まあ、セマンティックドメインにはIS-Aの関係があるように思えますが、サブタイプ/継承を使用してこれをモデル化することには、特にランタイムタイプリフレクションのため、少し警戒心があります。しかし、あなたは間違ったことを恐れていると思います。サブタイピングには確かに危険が伴いますが、実行時にオブジェクトをクエリしているという事実は問題ではありません。私の言っていることがわかります。
オブジェクト指向プログラミングはIS-A関係の概念にかなり依存しており、おそらくそれにあまりにも依存しすぎて、2つの有名な重要な概念に至っています。
しかし、ISとAの関係を調べるための、より機能的なプログラミングベースの方法が他にもあると思います。おそらくこれらの問題はありません。最初に、プログラムで馬とユニコーンをモデル化するため、Horse
型とUnicorn
型を作成します。これらのタイプの値は何ですか?まあ、私はこれを言うでしょう:
それは当たり前のことのように思えるかもしれませんが、人々が円楕円問題のような問題に陥る方法の1つは、それらの点を十分に注意深く考慮しないことです。すべての円は楕円ですが、それは、円のすべてのスキーマ化された説明が、自動的に別のスキーマによる楕円のスキーマ化された説明であることを意味しません。つまり、円が楕円であっても、Circle
がEllipse
であることを意味しているわけではありません。しかし、それは次のことを意味します:
Circle
(体系化された円の説明)を、同じ円を説明するEllipse
(さまざまな種類の説明)に変換する合計関数があります。Ellipse
を受け取り、円を表す場合、対応するCircle
を返す部分関数があります。したがって、関数型プログラミングの用語では、Unicorn
タイプはHorse
のサブタイプである必要はなく、次のような操作が必要です。
-- Convert any Unicorn-description of into a horse-description that
-- describes the same unicorns.
toHorse :: Unicorn -> Horse
-- If the horse described by the given horse-description is a Unicorn,
-- then return a Unicorn-description of that Unicorn, otherwise return
-- nothing.
toUnicorn :: Horse -> Maybe Unicorn
そして、toUnicorn
はtoHorse
の正反対でなければなりません:
toUnicorn (toHorse x) = Just x
HaskellのMaybe
型は、他の言語が「オプション」型と呼ぶものです。たとえば、Java 8 Optional<Unicorn>
タイプはUnicorn
か、何もありません。例外をスローするか、「デフォルトまたはマジック値」を返す)の2つの選択肢は非常にオプションタイプに似ています。
基本的にここで私が行ったことは、サブタイプや継承を使用せずに、タイプと関数の観点からIS-A関係の概念を再構築することです。私がこれから取り上げるのは:
Horse
タイプが必要です。Horse
型は、値がユニコーンを表すかどうかを明確に判断するために十分な情報をエンコードする必要があります。Horse
タイプの一部の操作では、そのタイプのクライアントが特定のHorse
がユニコーンであるかどうかを監視できるように、その情報を公開する必要があります。Horse
タイプのクライアントは、実行時にこれらの後者の操作を使用して、ユニコーンと馬を区別する必要があります。したがって、これは基本的に「すべてのHorse
に、それがユニコーンであるかどうかを尋ねる」モデルです。あなたはそのモデルに警戒していますが、私は間違っていると思います。 Horse
sのリストを提供すると、タイプが保証することはすべて、リスト内のアイテムが馬であることです。そのため、必然的に、実行時に何かを実行して、どれがユニコーンであるかを通知する必要があります。したがって、それを回避することはできないと思います。それを行う操作を実装する必要があります。
オブジェクト指向プログラミングでは、これを行う一般的な方法は次のとおりです。
Horse
タイプがあります。Unicorn
のサブタイプとしてHorse
を使用します。Horse
がUnicorn
であるかどうかを判別するクライアントアクセス可能な操作として、ランタイムタイプリフレクションを使用します。これは、上で提示した「もの対説明」の角度から見ると、大きな弱点があります。
Horse
インスタンスがあり、Unicorn
インスタンスではない場合はどうなりますか?最初に戻ると、これは、このIS-A関係をモデル化するためにサブタイプとダウンキャストを使用することについて本当に怖い部分だと思います。ランタイムチェックを行う必要があるという事実ではありません。タイポグラフィを少し乱用し、Horse
インスタンスであるかどうかをUnicorn
で確認することは、Horse
でユニコーンであるかどうかを確認することと同じではありません(ユニコーンでもある馬のHorse
-説明であるかどうか)。あなたのプログラムがHorses
を構築するコードをカプセル化して、クライアントがUnicornを記述するHorse
を構築しようとするたびに、Unicorn
クラスがインスタンス化されるようにならない限り、そうではありません。私の経験では、プログラマがこれを注意深く行うことはほとんどありません。
したがって、Horse
sをUnicorn
sに変換する、ダウンキャストではない明示的な操作があるアプローチを使用します。これは、Horse
タイプのメソッドである可能性があります。
interface Horse {
// ...
Optional<Unicorn> toUnicorn();
}
...または外部オブジェクトである可能性があります(「馬がユニコーンであるかどうかを通知する、馬の個別のオブジェクト」):
class HorseToUnicornCoercion {
Optional<Unicorn> convert(Horse horse) {
// ...
}
}
これらのどちらを選択するかは、プログラムの編成方法の問題です。どちらの場合も、上記のHorse -> Maybe Unicorn
操作と同等であり、さまざまな方法でパッケージ化しているだけです(これは確かに波及効果をもたらします) Horse
タイプがクライアントに公開する必要がある操作)。