私は現在C#を習得しようとしているので、C#による適応コードGary McLean Hallを読んでいます。
彼はパターンとアンチパターンについて書いています。実装とインターフェースの部分で、彼は次のように書いています:
インターフェイスへのプログラミングの概念に慣れていない開発者は、多くの場合、インターフェイスの背後にあるものを手放すことが困難です。
コンパイル時に、インターフェイスのクライアントは、使用しているインターフェイスの実装を認識していないはずです。そのような知識は、クライアントをインターフェースの特定の実装に結びつける誤った仮定につながる可能性があります。
クラスが永続ストレージにレコードを保存する必要がある一般的な例を想像してみてください。これを行うために、使用する永続ストレージメカニズムの詳細を非表示にするインターフェースに正しく委任します。ただし、実行時に使用されているインターフェイスの実装について想定することは適切ではありません。たとえば、-インターフェイス参照を任意の実装にキャストすることは常に悪い考えです
それは言葉の壁かもしれないし、私の経験不足かもしれませんが、それがどういう意味かよくわかりません。これが私が理解していることです:
C#を練習するための自由時間の楽しいプロジェクトがあります。そこでクラスがあります:
public class SomeClass...
このクラスは多くの場所で使用されています。 C#を学びながら、インターフェイスで抽象化する方が良いと読みましたので、
public interface ISomeClass <- Here I made a "contract" of all the public methods and properties SomeClass needs to have.
public class SomeClass : ISomeClass <- Same as before. All implementation here.
そこで、いくつかのクラス参照をすべて調べ、それらをISomeClassに置き換えました。
私が書いた建設を除いて:
ISomeClass myClass = new SomeClass();
これが間違っていることを正しく理解していますか?はいの場合、その理由は何ですか?代わりに何をすべきですか?
クラスをインターフェイスに抽象化することは、そのインターフェイスの他の実装を作成するつもりであるか、将来的にそうする可能性が高い場合にのみ検討する必要があります。
したがって、SomeClass
とISomeClass
は、OracleObjectSerializer
クラスとIOracleObjectSerializer
インターフェースを持つようなものになるため、悪い例です。
より正確な例は、OracleObjectSerializer
やIObjectSerializer
のようなものです。プログラムで使用する実装を気にする唯一の場所は、インスタンスの作成時です。時々、これはファクトリー・パターンを使用することによってさらに分離されます。
プログラムの他のすべての場所では、IObjectSerializer
を使用する必要があります。ここで、SQLServerObjectSerializer
に加えてOracleObjectSerializer
の実装もあるとします。ここで、設定する特別なプロパティを設定する必要があり、そのメソッドはOracleObjectSerializerにのみ存在し、SQLServerObjectSerializerには存在しないとします。
それに対処する方法は2つあります。間違った方法と Liskov置換原理 アプローチです。
誤った方法、および本で言及されているまさにそのインスタンスは、IObjectSerializer
のインスタンスを取得してOracleObjectSerializer
にキャストし、メソッドsetProperty
を呼び出すことです。 OracleObjectSerializer
。 それでもインスタンスがOracleObjectSerializer
であることがわかっている可能性があるため、これは悪いことです。これは、プログラムに、実装がどのようなものであるかを知りたい別のポイントを導入することになります。その実装が変更された場合、そしておそらく複数の実装がある場合は遅かれ早かれベストケースのシナリオで、これらすべての場所を見つけて適切な調整を行う必要があります。最悪のシナリオでは、IObjectSerializer
インスタンスをOracleObjectSerializer
にキャストし、本番環境でランタイムエラーを受け取ります。
Liskovは、適切に実行された場合、私のsetProperty
の場合のように、実装クラスにはOracleObjectSerializer
のようなメソッドは絶対に必要ないはずだと述べました。クラスをOracleObjectSerializer
からIObjectSerializer
に抽象化する場合、そのクラスを使用するために必要なすべてのメソッドを含める必要があります。それができない場合は、抽象化に問題があります( Dog
クラスは、たとえばIPerson
実装として機能します)。
正しいアプローチは、setProperty
にIObjectSerializer
メソッドを提供することです。 SQLServerObjectSerializer
の同様のメソッドは、理想的にはこのsetProperty
メソッドを通じて機能します。さらに良いのは、Enum
を使用してプロパティ名を標準化することです。各実装は、列挙型を独自のデータベース用語と同等のものに変換します。
簡単に言えば、ISomeClass
を使用するのはその半分にすぎません。作成を担当するメソッドの外部でキャストする必要はありません。そうすることは、ほぼ間違いなく重大な設計ミスです。
受け入れられた答えは正しく、非常に便利ですが、あなたが尋ねたコード行について具体的に簡単に説明したいと思います。
ISomeClass myClass = new SomeClass();
大まかに言って、これはひどいことではありません。可能な限り回避すべきことはこれを行うことです:
void someMethod(ISomeClass interface){
SomeClass cast = (SomeClass)interface;
}
あなたのコードが外部的にインターフェースを提供されているが、内部的に特定の実装にキャストする場合、「私はそれがその実装のみであることを知っているからです」。それが本当であったとしても、インターフェースを使用して実装にキャストすることにより、自発的に実際の型の安全性を放棄して、抽象化を使用するふりをすることができます。他の誰かが後でコードに取り組み、インターフェイスパラメーターを受け入れるメソッドを見つけた場合、そのインターフェイスの実装はすべて、有効なオプションであると想定します。特定のメソッドが必要なパラメータについてあることを忘れた後の行。インターフェースから特定の実装にキャストする必要性を感じた場合、インターフェース、実装、またはそれらを参照するコードのいずれかが正しく設計されておらず、変更する必要があります。たとえば、渡された引数が特定のクラスである場合にのみメソッドが機能する場合、パラメーターはそのクラスのみを受け入れる必要があります。
ここで、コンストラクター呼び出しを振り返ってみましょう
ISomeClass myClass = new SomeClass();
キャストの問題は実際には当てはまりません。これは外部に公開されているようには見えないため、それに関連する特定のリスクはありません。基本的に、このコード行自体は、インターフェースが最初から抽象化されるように設計された実装の詳細であるため、外部の監視者は、何をするかに関係なく同じように機能することを確認します。ただし、これはインターフェイスの存在から何も得ません。 myClass
のタイプはISomeClass
ですが、常に特定の実装SomeClass
が割り当てられているため、理由はありません。コンストラクター呼び出しだけを変更することでコード内の実装を交換できる、または後で別の実装にその変数を再割り当てできるなど、マイナーな潜在的な利点がいくつかありますが、変数をインターフェイスではなくインターフェイスに入力する必要がある場所がない限りこのパターンの実装により、コードはインターフェースが実際に使用されただけのように見え、インターフェースの利点を実際に理解しているわけではありません。
明確にするために、キャストを定義しましょう。
キャストとは、何かをあるタイプから別のタイプに強制的に変換することです。一般的な例は、浮動小数点数を整数型にキャストすることです。キャスト時に特定の変換を指定できますが、デフォルトでは単にビットを再解釈します。
このMicrosoft docsページ からキャストする例を次に示します。
// Create a new derived type.
Giraffe g = new Giraffe();
// Implicit conversion to base type is safe.
Animal a = g;
// Explicit conversion is required to cast back
// to derived type. Note: This will compile but will
// throw an exception at run time if the right-side
// object is not in fact a Giraffe.
Giraffe g2 = (Giraffe) a;
あなたcouldは同じことを行い、インターフェースを実装する何かをそのインターフェースの特定の実装にキャストしますが、あなた期待とは異なる実装が使用されている場合、エラーまたは予期しない動作が発生するため、にするべきではありません。
私はあなたのコードを悪い例で示す方が簡単だと思います:
public interface ISomeClass
{
void DoThing();
}
public class SomeClass : ISomeClass
{
public void DoThing()
{
// Mine for BitCoin
}
}
public class AnotherClass : ISomeClass
{
public void DoThing()
{
// Mine for oil
}
public Decimal Depth;
}
void main()
{
ISomeClass task = new SomeClass();
task.DoThing(); // This is good
Console.WriteLine("Depth = {0}", ((AnotherClass)task).Depth); <-- The task object will not have this field
}
問題は、最初にコードを記述したとき、おそらくそのインターフェイスの実装が1つしかないため、キャストが引き続き機能することです。将来的には、別のクラスを実装し、(私の例が示すように)使用しているオブジェクトに存在しないデータにアクセスしてみてください。
私の5セント:
これらの例はすべてOKですが、実際の例ではなく、実際の意図を示していません。
C#がわからないので、抽象的な例を示します(JavaとC++の組み合わせ)。
インターフェースiList
があるとします:
interface iList<Key,Value>{
bool add(Key k, Value v);
bool remove(Element e);
Value get(Key k);
}
たくさんの実装があるとしましょう:
多くの異なる実装を考えることができます。
次のコードがあるとします。
uint begin_size = 1000;
iList list = new DynamicArrayList(begin_size);
iList
を使用したいという意図を明確に示しています。もちろん、DynamicArrayList
固有の操作は実行できなくなりましたが、iList
が必要です。
次のコードを検討してください。
iList list = factory.getList();
今、私たちは実装が何であるかさえ知りません。この最後の例は、ディスクからファイルをロードし、そのファイルタイプ(gif、jpeg、png、bmp ...)を必要としない場合の画像処理でよく使用されますが、必要なのは画像操作(フリップ、スケール、最後にpngとして保存)。
ISomeClassインターフェースがあり、そのオブジェクトmyObjectは、ISomeClassを実装するように宣言されていることを除いて、コードからは何もわかりません。
ISomeClassインターフェイスを実装していることがわかっているクラスSomeClassがあります。 ISomeClassを実装するように宣言されているか、ISomeClassを実装するために自分で実装したためです。
MyClassをSomeClassにキャストすることの何が問題になっていますか? 2つの点が間違っています。 1つは、myClassがSomeClass(SomeClassのインスタンスまたはSomeClassのサブクラスのインスタンス)に変換できるものであることを本当に知らないため、キャストが失敗する可能性があります。 2つは、これを行う必要はありません。 iSomeClassとして宣言されたmyClassを使用して、ISomeClassメソッドを使用する必要があります。
SomeClassオブジェクトを取得するポイントは、インターフェースメソッドが呼び出されたときです。ある時点で、インターフェイスで宣言されているmyClass.myMethod()を呼び出しますが、SomeClass、およびもちろんISomeClassを実装する他の多くのクラスにも実装があります。呼び出しがSomeClass.myMethodコードで終了する場合は、selfがSomeClassのインスタンスであることがわかります。その時点で、SomeClassオブジェクトとして使用することは完全に問題なく、実際に正しく行われます。もちろん、それが実際にはSomeClassではなく、OtherClassのインスタンスである場合、SomeClassコードには到達しません。