web-dev-qa-db-ja.com

型テストはいつ大丈夫ですか?

いくつかの固有の型安全性を備えた言語(JavaScriptなどではない)を想定します。

SuperTypeを受け入れるメソッドがあれば、ほとんどの場合、アクションを選択するために型テストを実行したくなる可能性があることがわかります。

_public void DoSomethingTo(SuperType o) {
  if (o isa SubTypeA) {
    o.doSomethingA()
  } else {
    o.doSomethingB();
  }
}
_

通常、常にではありませんが、SuperTypeに単一のオーバーライド可能なメソッドを作成し、これを行う必要があります。

_public void DoSomethingTo(SuperType o) {
  o.doSomething();
}
_

...ここで、各サブタイプには独自のdoSomething()実装が与えられます。アプリケーションの残りの部分は、指定されたSuperTypeが実際にSubTypeAであるかSubTypeBであるかを適切に無視できます。

素晴らしい。

ただし、すべてではないにしても、ほとんどのタイプセーフ言語では、_is a_に似た操作が提供されます。そして、それは明示的な型テストの潜在的な必要性を示唆しています。

つまり、どのような状況で、もしあれば、する必要があります私たちまたはmust明示的な型テストを実行しますか?

私の心の欠如や創造性の欠如を許してください。私は以前にそれをしたことがあるのを知っています。でも、正直言ってずっと前のことで良かったか思い出せない!そして、最近の記憶では、カウボーイJavaScriptの外部で型をテストするためのニーズに遭遇したことはないと思います。

53
svidgen

「型テストは大丈夫ですか?」に対する標準的な答えは「決してない」です。これを証明または反証する方法はありません。それは、「優れたデザイン」または「優れたオブジェクト指向デザイン」を作るものについての信念体系の一部です。それもホクムです。

確かに、クラスの統合されたセットと、そのような直接型テストを必要とする1つまたは2つ以上の関数がある場合、おそらくそれは間違っています。本当に必要なのは、SuperTypeとそのサブタイプで異なる方法で実装されているメソッドです。これはオブジェクト指向プログラミングの一部であり、理由クラス全体と継承が存在します。

この場合、型のテストが本質的に間違っているのではなく、明示的に型のテストが間違っていますが、言語にはすでに型の識別を実現する明確で拡張可能な慣用的な方法があり、それを使用していません。代わりに、原始的で壊れやすく、拡張できないイディオムにフォールバックしました。

解決策:イディオムを使用します。提案したように、各クラスにメソッドを追加してから、標準の継承とメソッド選択のアルゴリズムに、どちらのケースが適用されるかを判断させます。または、基本タイプを変更できない場合は、サブクラスを作成し、そこにメソッドを追加します。

従来の知恵と、いくつかの答えについてはこれで終わりです。明示的な型テストが理にかなっているいくつかのケース:

  1. これは1回限りのものです。行うべき型の区別がたくさんある場合は、型またはサブクラスを拡張できます。しかし、そうではありません。明示的なテストが必要な箇所は1つか2つしかないので、戻ってクラス階層を調べて関数をメソッドとして追加するのは時間の無駄です。または、そのような単純で限られた使用法のために、基本クラスの一般性、テスト、設計レビュー、ドキュメント、またはその他の属性を追加することは、実際的な努力に値しません。その場合、直接テストを行う関数を追加するのが合理的です。

  2. クラスを調整することはできません。サブクラス化について考えますが、それはできません。たとえば、Javaの多くのクラスはfinalと指定されています。 _public class ExtendedSubTypeA extends SubTypeA {...}_をスローしようとすると、コンパイラーは不明確な言葉ではなく、実行していることは不可能であることを通知します。オブジェクト指向モデルの優雅さと洗練さ!タイプを拡張できないと誰かが決めました!残念ながら、標準ライブラリの多くはfinalであり、クラスの作成finalは一般的な設計ガイダンスです。関数end-runは、利用可能なままになっているものです。

    ところで、これは静的に型付けされた言語に限定されません。動的言語Pythonには、Cで実装されたカバーの下では実際には変更できない多数の基本クラスがあります。Javaのように、ほとんどの標準型が含まれています。

  3. コードは外部コードです。制御できないデータベースサーバー、ミドルウェアエンジン、その他のコードベースのクラスとオブジェクトを使用して開発していますまたは調整します。あなたのコードは、他の場所で生成されたオブジェクトをあまり消費していません。 SuperTypeをサブクラス化できたとしても、サブクラスでオブジェクトを生成するために依存するライブラリを取得することはできません。彼らはあなたが知っているタイプのインスタンスをあなたに手渡すでしょう、あなたのバリアントではありません。これは常に当てはまるわけではありません...それらは時々柔軟性のために構築され、それらはあなたがそれらを供給するクラスのインスタンスを動的にインスタンス化します。または、それらのファクトリが構築するサブクラスを登録するメカニズムを提供します。 XMLパーサーは、そのようなエントリポイントを提供するのに特に優れているようです。たとえば、 JavaのJAXBの例 または Pythonのlxml 。しかし、ほとんどのコードベースはそのような拡張機能を提供しません。彼らはあなたが彼らが構築されたクラスとあなたが知っているクラスをあなたに手渡すでしょう。純粋にオブジェクト指向のタイプセレクターを使用できるようにするためだけに、それらの結果をカスタム結果にプロキシすることは一般的に意味がありません。タイプの識別を行う場合は、比較的大まかに行う必要があります。型テストコードは非常に適切に見えます。

  4. 貧しい人のジェネリックス/複数のディスパッチ。さまざまな異なる型をコードに受け入れ、非常に型固有のメソッドの配列があると感じたい優雅ではありません。 public void add(Object x)は論理的に見えますが、addByteaddShortaddIntaddLongaddFloat、の配列ではありませんaddDoubleaddBooleanaddChar、およびaddStringバリアント(いくつか例を挙げると)。高度なスーパータイプを採用し、タイプごとに何をすべきかを決定する関数またはメソッドを持っている-彼らは毎年のBooch-Liskovデザインシンポジウムであなたに純度賞を受賞するつもりはありませんが、 ハンガリー語の命名 は、より単純なAPIを提供します。ある意味で、_is-a_または_is-instance-of_テストは、ネイティブまたはマルチディスパッチをネイティブでサポートしていない言語コンテキストでシミュレートしています。

    genericsducktyping の両方の組み込み言語サポートにより、「優雅で適切な何かを行う」可能性が高くなるため、型チェックの必要性が減少します。 JuliaやGoなどの言語で見られる multiple dispatch /インターフェース選択は、直接型テストを組み込みのメカニズムに置き換え、タイプベースの「何をするか」を選択できるようにします。ただし、すべての言語がこれらをサポートしているわけではありません。 Javaたとえば、通常は単一ディスパッチであり、そのイディオムはアヒルのタイピングにあまり適していません。

    しかし、これらのすべての型識別機能(継承、ジェネリック、ダックタイピング、およびマルチディスパッチ)を使用しても、オブジェクトの型に基づいて何かを実行しているという事実を作成する単一の統合ルーチンがあると便利な場合があります明確かつ即時。 metaprogramming では、本質的に避けられないことがわかりました。直接型問い合わせにフォールバックするかどうかは、「実用的なプラグマティズム」であるか「ダーティーコーディング」であるかは、設計哲学と信念によって異なります。

49
Jonathan Eunice

equals(other)メソッドなど、2つのオブジェクトを比較するときに、otherの正確なタイプに応じて異なるアルゴリズムが必要になる可能性があるので、これが必要だった主な状況です。それでも、それはかなりまれです。

非常にまれに、逆シリアル化または解析の後に発生したもう1つの状況は、より具体的な型に安全にキャストする必要がある場合です。

また、制御できないサードパーティのコードを回避するためのハックが必要な場合もあります。日常的に使いたくないものの1つですが、本当に必要なときにそこにあるとうれしいです。

25
Karl Bielefeldt

標準的な(ただし、まれに)ケースは次のようになります。

public void DoSomethingTo(SuperType o) {
  if (o isa SubTypeA) {
    DoSomethingA((SubTypeA) o )
  } else {
    DoSomethingB((SubTypeB) o );
  }
}

関数DoSomethingAまたはDoSomethingBは、SuperType/SubTypeA/SubTypeBの継承ツリーのメンバー関数として簡単に実装できません。たとえば、

  • サブタイプは、変更できないライブラリの一部です。または
  • そのライブラリにDoSomethingXXXのコードを追加すると、禁止された依存関係が導入されることになります。

この問題を回避できる状況がよくあることに注意してください(たとえば、SubTypeAおよびSubTypeBのラッパーまたはアダプターを作成するか、DoSomethingを完全に再実装しようとするSuperType)の基本的な操作の条件ですが、これらのソリューションは、明示的な型テストを行うよりも、面倒なものや、複雑で拡張性の低いものになる場合があります。

私の昨日の仕事の例:オブジェクトのリスト(タイプSuperTypeの、2つの異なるサブタイプを持つ)の処理を並列化しようとする状況がありました。もっと)。非並列化バージョンには2つのループが含まれていました。1つはサブタイプAのオブジェクト用のループで、DoSomethingAを呼び出し、もう1つはサブタイプBのオブジェクト用で、DoSomethingBを呼び出します。

「DoSomethingA」メソッドと「DoSomethingB」メソッドはどちらも、サブタイプAとBのスコープでは利用できないコンテキスト情報を使用する、時間のかかる計算です(そのため、サブタイプのメンバー関数として実装しても意味がありません)。新しい「並列ループ」の観点からは、それらを一律に処理することで物事が非常に簡単になるので、上からDoSomethingToに類似した関数を実装しました。ただし、「DoSomethingA」と「DoSomethingB」の実装を調べると、内部での動作が大きく異なります。したがって、多くの抽象メソッドでSuperTypeを拡張して汎用の「DoSomething」を実装しようとしても、実際には機能しなかった、または完全にオーバーデザインすることを意味していました。

12
Doc Brown

ボブおじさんがそれを呼ぶように:

_When your compiler forgets about the type.
_

Clean Coderエピソードの1つで、Employeesを返すために使用される関数呼び出しの例を挙げました。 ManagerEmployeeのサブタイプです。 ManagerのIDを受け入れ、オフィスに呼び出すアプリケーションサービスがあるとします。関数getEmployeeById()はスーパータイプEmployeeを返しますが、確認したいこのユースケースでマネージャーが返された場合。

例えば:

_var manager = employeeRepository.getEmployeeById(empId);
if (!(manager is Manager))
   throw new Exception("Invalid Id specified.");
manager.summon();
_

ここでは、クエリによって返された従業員が実際にマネージャーであるかどうかを確認しています(つまり、マネージャーであると予想され、それ以外の場合はすぐに失敗します)。

良い例ではありませんが、結局ボブおじさんです。

更新

メモリから覚えられる限り、例を更新しました。

5
Songo

タイプチェックはいつOKですか?

絶対にしない

  1. is句を変更することでタイプの既存の動作を自由に変更できるため、他の関数でそのタイプに関連付けられた動作を行うことで、 Open Closed Principle に違反しています。一部の言語、またはシナリオによっては)isチェックを実行する関数の内部を変更しないと型を拡張できないためです。
  2. さらに重要なことに、isチェックは、 Liskov Substitution Principle に違反していることを強く示しています。 SuperTypeで機能するものはすべて、存在する可能性のあるサブタイプを完全に無視する必要があります。
  3. あなたは暗黙的にいくつかの振る舞いを型の名前に関連付けています。これらの暗黙的なコントラクトはコード全体に分散され、実際のクラスメンバーのように普遍的かつ一貫して適用されることが保証されないため、これによりコードの維持が難しくなります。

そうは言っても、isのチェックは、他の方法よりも悪くない場合があります。すべての一般的な機能を基本クラスに入れるのは重労働であり、多くの場合、より深刻な問題につながります。インスタンスの「タイプ」のフラグまたは列挙型を持つ単一のクラスを使用することは、タイプシステムの迂回をallに広げているため、恐ろしいよりも悪いです消費者。

つまり、型チェックは常にコードの匂いが強いと考える必要があります。しかし、すべてのガイドラインと同様に、どのガイドライン違反が最も攻撃的でないかを選択せざるを得ない場合があります。

3
Telastyn

大規模なコードベース(10万行を超えるコード)を取得し、出荷に近づいている、または後でマージする必要があるブランチで作業している場合、多くの対処方法を変更するには多大なコスト/リスクが伴います。

次に、システムの大規模な屈折器、または単純なローカライズされた「タイプテスト」のオプションが時々あります。これは、できるだけ早く返済すべき技術的負債を生み出しますが、多くの場合そうではありません。

(例として使用するのに十分なほど小さいコードは、優れたデザインを明確に表示するのに十分小さいため、例を思い付くことは不可能です。)

または、言い換えると、目的がデザインのクリーンさに対する「賛成票」を獲得することではなく、賃金を支払うになることです。


もう1つの一般的なケースはUIコードです。たとえば、あるタイプの従業員に異なるUIを表示するが、UIの概念をすべての「ドメイン」クラスにエスケープしたくない場合です。

「タイプテスト」を使用して、表示するUIのバージョンを決定したり、「ドメインクラス」から「UIクラス」に変換する洗練されたルックアップテーブルを用意したりできます。ルックアップテーブルは、「型テスト」を1か所に隠す方法の1つにすぎません。

(データベース更新コードにはUIコードと同じ問題がありますが、データベース更新コードは1セットしかない傾向がありますが、表示されるオブジェクトのタイプに適応する必要があるさまざまな画面が多数ある場合があります。)

2
Ian

LINQの実装は、可能なパフォーマンス最適化のために多くの型チェックを使用し、次にIEnumerableのフォールバックを使用します。

最も明白な例は、おそらくElementAtメソッドです(.NET 4.5ソースの小さな抜粋)。

public static TSource ElementAt<TSource>(this IEnumerable<TSource> source, int index) { 
    IList<TSource> list = source as IList<TSource>;

    if (list != null) return list[index];
    // ... and then an enumerator is created and MoveNext is called index times

しかし、Enumerableクラスには、同様のパターンが使用される場所がたくさんあります。

したがって、おそらく一般的に使用されるサブタイプのパフォーマンスを最適化することは有効な用途です。これがどのように設計されていたのかはわかりません。

ゲーム開発、特に衝突検出で頻繁に発生する例があり、これはある種の型テストを使用しないと対処することが困難です。

すべてのゲームオブジェクトが共通の基本クラスGameObjectから派生すると仮定します。各オブジェクトはリジッドボディの衝突形状CollisionShapeを持っていますが、これは共通のインターフェース(クエリの位置、方向など)を提供しますが、実際の衝突形状はすべて、SphereBoxConvexHullなどの具体的なサブクラスになり、それに固有の情報が格納されます。幾何学的オブジェクトのタイプ(実際の例については ここ を参照)

次に、衝突をテストするために、衝突形状タイプのペアごとに関数を作成する必要があります。

detectCollision(Sphere, Sphere)
detectCollision(Sphere, Box)
detectCollision(Sphere, ConvexHull)
detectCollision(Box, ConvexHull)
...

これらには、これら2つのジオメトリタイプの共通部分を実行するために必要な特定の数学が含まれています。

ゲームループの各「ティック」で、オブジェクトのペアの衝突をチェックする必要があります。しかし、私はGameObjectsとそれに対応するCollisionShapesにしかアクセスできません。明らかに、どの衝突検出関数を呼び出すかを知るために、具体的な型を知る必要があります。 ダブルディスパッチ (論理的にタイプをチェックすることと論理的に何の違いもない)でも、ここでは役に立ちません*。

実際、この状況で私が見た物理エンジン(BulletとHavok)は、何らかの形式の型テストに依存しています。

これは必ずしも良いソリューションであると言っているわけではありません。それは、この問題に対して考えられる少数のソリューションの中で最良であるということだけです。

*技術的にはisN(N + 1)/ 2の組み合わせを必要とする恐ろしく複雑な方法でダブルディスパッチを使用できます(Nはあなたが持っている形状タイプの数)、そしてあなたが実際に何をしているのかを難読化するだけであり、同時に2つの形状のタイプを見つけているので、これを現実的な解決策とは見なしません。

1
Martin

特定のタスクを実行することは実際には彼らの責任ではないため、すべてのクラスに共通のメソッドを追加したくない場合があります。

たとえば、いくつかのエンティティを描画したいが、描画コードをそれらに直接追加したくない場合(これは理にかなっています)。複数のディスパッチをサポートしていない言語では、次のコードになる可能性があります。

void DrawEntity(Entity entity) {
    if (entity instanceof Circle) {
        DrawCircle((Circle) entity));
    else if (entity instanceof Rectangle) {
        DrawRectangle((Rectangle) entity));
    } ...
}

これは、このコードが複数の場所に表示され、新しいエンティティタイプを追加するときにどこでも変更する必要がある場合に問題になります。その場合は、Visitorパターンを使用することで回避できますが、過度に設計するのではなく、単純にしておく方がよい場合もあります。これらは、型テストがOKである状況です。

1
Honza Brabec

型テストと型キャストは、非常に密接に関連する2つの概念です。密接に関連しているので、never型テストを実行する必要があるでない限り意図しない限り、型キャストを実行する必要があると確信しています結果に基づくオブジェクト。

理想的なオブジェクト指向設計について考えるとき、型のテスト(およびキャスト)shouldは決して発生しません。しかし、うまくいけば、これでオブジェクト指向プログラミングは理想的ではないことがわかりました。時には、特に下位レベルのコードでは、コードが理想に忠実であり続けることができない場合があります。これは、JavaのArrayListの場合です。実行時に配列に格納されているクラスがわからないため、Object[]配列を作成し、静的に正しい型にキャストします。

型テスト(および型キャスト)の一般的なニーズはEqualsメソッドから来ることが指摘されています。これは、ほとんどの言語でプレーンなObjectを使用することになっています。実装には、2つのオブジェクトが同じ型であるかどうかを確認するための詳細なチェックがいくつか必要です。

型テストもよく反映されます。多くの場合、Object[]またはその他の汎用配列を返すメソッドがあり、何らかの理由ですべてのFooオブジェクトを引き出したい場合があります。これは、型テストとキャストの完全に正当な使用法です。

一般的に、型のテストは、コードを特定の実装の作成方法に不必要に結合する場合、badです。これは、線、四角形、円の交点を見つけたい場合や、交点関数の組み合わせごとに異なるアルゴリズムを使用する場合など、タイプまたはタイプの組み合わせごとに特定のテストが必要になる場合があります。 1つの種類のオブジェクトに固有の詳細をそのオブジェクトと同じ場所に配置することが目標です。これにより、コードの保守と拡張が容易になります。

0
meustrus

私が使う唯一の時間は、反射との組み合わせです。しかし、それでもほとんどの場合、特定のクラスにハードコードされていない(またはStringListなどの特別なクラスにのみハードコードされている)動的チェックです。

動的チェックとは:

boolean checkType(Type type, Object object) {
    if (object.isOfType(type)) {

    }
}

ハードコードされていません

boolean checkIsManaer(Object object) {
    if (object instanceof Manager) {

    }
}
0
m3th0dman

型テストはツールであり、賢く使用して強力な同盟者になることができます。それをうまく使用しないと、コードが臭い始めます。

私たちのソフトウェアでは、リクエストに応じてネットワーク経由でメッセージを受信しました。すべての逆シリアル化されたメッセージは、共通の基本クラスMessageを共有しました。

クラス自体は非常にシンプルで、型付けされたC#プロパティとしてのペイロードと、それらをマーシャリングおよびアンマーシャリングするためのルーチン(実際、メッセージ形式のXML記述からt4テンプレートを使用してほとんどのクラスを生成しました)

コードは次のようになります:

Message response = await PerformSomeRequest(requestParameter);

// Server (out of our control) would send one message as response, but 
// the actual message type is not known ahead of time (it depended on 
// the exact request and the state of the server etc.)
if (response is ErrorMessage)
{ 
    // Extract error message and pass along (for example using exceptions)
}
else if (response is StuffHappenedMessage)
{
    // Extract results
}
else if (response is AnotherThingHappenedMessage)
{
    // Extract another type of result
}
// Half a dozen other possible checks for messages

確かに、メッセージアーキテクチャはより適切に設計できると主張することはできますが、それはずっと前に設計されたものであり、C#用ではないため、それがそうです。ここで型テストは、あまりにも粗末な方法で私たちにとって本当の問題を解決しました。

注目に値するのは、C#7.0が パターンマッチング (多くの点でステロイドの型テストである)を取得していることです。

0
Isak Savo

汎用のJSONパーサーを取ります。正常な解析の結果は、配列、辞書、文字列、数値、ブール値、またはnull値です。それらのどれでもかまいません。また、配列の要素またはディクショナリの値も、これらのタイプのいずれかになります。データはプログラムの外部から提供されるため、結果を受け入れる必要があります(つまり、クラッシュせずに受け入れる必要があります。can予期しない結果を拒否します)。

0
gnasher729

2つのタイプを含む決定を行う必要があり、この決定がそのタイプ階層の外側のオブジェクトにカプセル化されている場合は許容されます。たとえば、処理を待機しているオブジェクトのリストで、次に処理されるオブジェクトをスケジュールするとします。

abstract class Vehicle
{
    abstract void Process();
}

class Car : Vehicle { ... }
class Boat : Vehicle { ... }
class Truck : Vehicle { ... }

ここで、ビジネスロジックが文字どおり「すべての車がボートやトラックよりも優先される」としましょう。クラスにPriorityプロパティを追加すると、このビジネスロジックをきれいに表現できなくなります。

abstract class Vehicle
{
    abstract void Process();
    abstract int Priority { get }
}

class Car : Vehicle { public Priority { get { return 1; } } ... }
class Boat : Vehicle { public Priority { get { return 2; } } ... }
class Truck : Vehicle { public Priority { get { return 2; } } ... }

問題は、優先順位を理解するためにすべてのサブクラスを確認する必要がある、つまり、サブクラスに結合を追加したことです。

もちろん、優先順位を定数にして、それ自体をクラスに入れる必要があります。これにより、ビジネスロジックのスケジューリングを維持できます。

static class Priorities
{
    public const int CAR_PRIORITY = 1;
    public const int BOAT_PRIORITY = 2;
    public const int TRUCK_PRIORITY = 2;
}

ただし、実際には、スケジューリングアルゴリズムは将来変更される可能性があるものであり、最終的にはタイプだけに依存するわけではありません。たとえば、「5000 kgを超えるトラックは、他のすべての車両よりも特別な優先順位を取得します」と表示される場合があります。これが、スケジューリングアルゴリズムが独自のクラスに属している理由であり、タイプを調べて最初に実行する必要があるものを判別するのは良いアイデアです

class VehicleScheduler : IScheduleVehicles
{
    public Vehicle WhichVehicleGoesFirst(Vehicle vehicle1, Vehicle vehicle2)
    {
        if(vehicle1 is Car) return vehicle1;
        if(vehicle2 is Car) return vehicle2;
        return vehicle1;
    }
}

これは、ビジネスロジックを実装する最も簡単な方法であり、将来の変更に対しても最も柔軟です。

0
Scott Whitlock