定義としてC#の抽象クラスを使用する


21

C ++開発者として、私はC ++ヘッダーファイルに非常に慣れており、コード内に何らかの「ドキュメント」を強制することが有益であることがわかりました。そのため、私は通常、C#コードを読む必要があるときに悪い時間を過ごします。私が作業しているクラスのそのようなメンタルマップを持っていません。

ソフトウェアエンジニアとして、プログラムのフレームワークを設計していると仮定しましょう。C ++ヘッダーで行うのと同様に、すべてのクラスを抽象的な未実装クラスとして定義し、開発者に実装させるのはあまりにもクレイジーでしょうか?

誰かがこれをひどい解決策だと思う理由がいくつかあるのではないかと推測していますが、その理由はわかりません。このようなソリューションでは、何を考慮する必要がありますか?


13
C#は、C ++とはまったく異なるプログラミング言語です。一部の構文は似ていますが、適切に機能するにはC#を理解する必要があります。そして、ヘッダーファイルに依存するという悪い習慣について何かする必要があります。私は何十年もC ++を使ってきましたが、ヘッダーファイルは一度も読んだことがなく、書きました。
ベント

17
C#とJavaの優れた点の1つは、ヘッダーファイルからの自由です!良いIDEを使用してください。
エリックエイド

9
C ++ヘッダーファイルの宣言は、生成されたバイナリの一部にはなりません。コンパイラとリンカ用にあります。これらのC#抽象クラスは、生成されたコードの一部であり、何の利点もありません。
マークベニングフィールド

16
C#の同等の構成体は、抽象クラスではなくインターフェイスです。
ロバートハーベイ

6
@DaniBarcaCasafont「ヘッダーは、クラスのメソッドと属性が何であるか、どのタイプのパラメーターが期待し、何が返されるかを確認するための高速で信頼できる方法だと考えています」。Visual Studioを使用する場合、これに代わる方法はCtrl-MOショートカットです。
wha7ever-モニカの復元

回答:


44

これがC ++で行われた理由は、コンパイラをより速く、より簡単に実装できるようにすることに関係していました。これはプログラミングを簡単にするための設計ではありませんでした。

ヘッダーファイルの目的は、コンパイラがすべての期待される関数名を知るための超高速の最初のパスを実行し、それらを定義するクラスが持っていたとしても、cpファイルで呼び出されたときに参照できるようにそれらにメモリ位置を割り当てることでしたまだ解析されていません。

最新の開発環境で古いハードウェア制限の結果を再現しようとすることはお勧めしません!

すべてのクラスにインターフェースまたは抽象クラスを定義すると、生産性が低下します。その時間に他に何ができましたか?また、他の開発者はこの規則に従いません。

実際、他の開発者が抽象クラスを削除する場合があります。これらの両方の基準を満たすコードでインターフェイスを見つけた場合、それを削除し、コードからリファクタリングします。1. Does not conform to the interface segregation principle 2. Only has one class that inherits from it

もう1つは、Visual Studioに含まれているツールで、目的を自動的に達成することです。

  1. Class View
  2. Object Browser
  3. ではSolution Explorer、三角形をクリックしてクラスを使い、それらの関数、パラメーター、戻り値の型を確認できます。
  4. ファイルタブの下に3つの小さなドロップダウンメニューがあり、最も右側のメニューには現在のクラスのすべてのメンバーがリストされます。

C#でC ++ヘッダーファイルの複製に時間を割く前に、上記のいずれかを試してください。


さらに、これを行わない技術的な理由があります...最終的なバイナリが必要以上に大きくなります。マーク・ベニングフィールドによるこのコメントを繰り返します。

C ++ヘッダーファイルの宣言は、生成されたバイナリの一部にはなりません。コンパイラとリンカ用にあります。これらのC#抽象クラスは、生成されたコードの一部であり、何の利点もありません。


また、Robert Harveyによると、技術的には、C#のヘッダーに最も近いものは、抽象クラスではなくインターフェイスです。


2
また、不必要な仮想ディスパッチ呼び出しが大量に発生することになります(コードがパフォーマンスに敏感な場合は良くありません)。
ルーカスTrzesniewski

5
ここに言及されているインターフェイス分離の原則です。
NH。

2
クラス全体を単純に折りたたんで(右クリック->アウトライン->定義に折りたたむ)、鳥瞰図を取得することもできます。
jpmc26

「そのとき、他に何ができたでしょうか」->ええと、コピーアンドペーストと非常に小さな後処理。約3秒かかります。もちろん、その時間を使用して、代わりにタバコを巻く準備としてフィルターの先端を取り出すこともできます。それほど重要なポイントではありません。[免責事項:慣用的なC#と慣用的なC ++、およびその他の言語の両方が大好きです]
フレネル

1
@phresnel:クラスの定義を繰り返して、3秒で抽象クラスから元のクラスを継承し、簡単に鳥瞰図を表示できないほど十分に大きいクラスについては、エラーなしで確認してくださいそれ(OPが提案しているユースケースである:おそらく複雑でないクラスを複雑にしません)。ほぼ本質的に、元のクラスの複雑な性質は、抽象クラスの定義を暗記するだけではできないことを意味します(それは既に暗記されていることを意味するため)。そして、コードベース全体でこれを行うのに必要な時間を考慮してください。
フラット

14

まず、純粋な抽象クラスは、実際には単なる多重継承ができないインターフェースであることを理解してください。

書き込みクラス、抽出インターフェースは、脳死の活動です。あまりにも多くのため、リファクタリングがあります。残念です。この「すべてのクラスがインターフェイスを取得する」パターンに従うと、混乱が発生するだけでなく、ポイントが完全に失われます。

インターフェースは、クラスができることを単に正式に修正したものと考えるべきではありません。インターフェースは、クライアントコードを使用することで必要とされる詳細によって課される契約と見なされる必要があります。

現在、それを実装するクラスが1つだけのインターフェースを書くのに、私はまったく問題ありません。実際には、クラスがまだそれを実装していないかどうかは気にしません。使用するコードに必要なものを考えているからです。インターフェイスは、使用するコードが要求するものを表現します。これらの期待を満たせば、後から何でも好きなことをすることができます。

現在、あるオブジェクトが別のオブジェクトを使用するたびにこれを行うわけではありません。境界を越えるときにこれを行います。あるオブジェクトが他のどのオブジェクトと通信しているかを正確に知りたくない場合に、それを行います。ポリモーフィズムが機能する唯一の方法です。クライアントコードが話しているオブジェクトが変更される可能性が高いと予想される場合に実行します。私が使用しているのがStringクラスの場合、私は確かにこれを行いません。Stringクラスは素晴らしく安定しているので、変更するのを防ぐ必要はありません。

抽象化ではなく具体的​​な実装と直接対話することを決定した場合、実装は変更しないことを信頼するのに十分安定していると予測しています。

そういうわけで、私は依存性反転の原則を気にする方法があります。盲目的にこれをすべてに適用するべきではありません。抽象化を追加するとき、クラスの実装がプロジェクトの存続期間中安定しているという選択を信用していないということです。

これはすべて、Open Closed Principleに準拠しようとしていることを前提としています。この原則は、確立されたコードを直接変更することに関連するコストが大きい場合にのみ重要です。人々がオブジェクトを切り離すことの重要性に異議を唱える主な理由の1つは、直接変更を行うときに全員が同じコストを経験するわけではないためです。コードベース全体を再テスト、再コンパイル、再配布するのが簡単な場合、直接変更で変更の必要性を解決することは、この問題を非常に魅力的に単純化する可能性があります。

この質問に対する単純な答えはありません。インターフェイスまたは抽象クラスは、すべてのクラスに追加する必要があるものではありません。実装するクラスの数を数えて、不要であると判断することはできません。それは変化に対処することです。つまり、あなたは未来を予測しているのです。間違えても驚かないでください。自分を隅に追いやることなく、できるだけシンプルにしましょう。

そのため、コードを読むためだけに抽象化を書かないでください。そのためのツールがあります。抽象化を使用して、分離が必要なものを分離します。


5

はい(1)不要なコードを導入するため(2)読者を混乱させるため、それはひどいでしょう。

C#でプログラミングする場合は、C#を読むのに慣れる必要があります。とにかく、他の人が書いたコードはこのパターンに従わないでしょう。


1

単体テスト

インターフェイスまたは抽象クラスでコードを乱雑にするのではなく、単体テストを書くことを強くお勧めします(他の理由で保証されている場合を除く)。

よく書かれた単体テストは、クラスのインターフェイス(ヘッダーファイル、抽象クラス、またはインターフェイスのように)を記述するだけでなく、たとえば、目的の機能も記述します。

例:ヘッダーファイルmyclass.hを次のように記述した場合:

class MyClass
{
public:
  void foo();
};

代わりに、c#で次のようなテストを作成します。

[TestClass]
public class MyClassTests
{
    [TestMethod]
    public void MyClass_should_have_method_Foo()
    {
        //Arrange
        var myClass = new MyClass();
        //Act
        myClass.Foo();
        //Verify
        Assert.Inconclusive("TODO: Write a more detailed test");
    }
}

この非常に単純なテストは、ヘッダーファイルと同じ情報を伝えます。(パラメータなしの関数「Foo」を持つ「MyClass」という名前のクラスが必要です)ヘッダーファイルはよりコンパクトですが、テストにはさらに多くの情報が含まれています。

警告:上級ソフトウェアエンジニアが他の開発者にTDDのような方法論で衝突を激しく解決する(失敗する)テストを提供するようなプロセスですが、あなたの場合、それは大きな改善になります。


ユニットテスト(分離テスト)を行うには、インターフェイスなしでモックが必要ですか?実際の実装からのモック作成をサポートするフレームワークは、ほとんどの場合、インターフェイスを使用して実装をモック実装と交換します。
ブラン

OP:sの目的にはモックが必要だとは思わない。これはTDDのツールとしての単体テストではなく、ヘッダーファイルの代替としての単体テストであることを忘れないでください。(もちろん、モックなどを使用した「通常の」単体テストに進化する可能性があります)
グラン

わかりました、もし私がOPの側だけを考慮するなら、私はよりよく理解します より一般的な適用の答えとしてあなたの答えを読んでください。明確化のためのThx!
ブラン

豊富なモックの必要性は、多くの場合、悪いデザインの指標である@Bulan
TheCatWhisperer

@TheCatWhispererはい、私はそれをよく知っているので、議論はありません、私はどこでもその文を作ったことを見ることができません:DIは一般的にテストについて話していました実際の実装、およびインターフェイスがない場合にモックする方法について他の手法があった場合。
ブラン
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.