web-dev-qa-db-ja.com

継承とデリゲートフィールドを介した拡張ポイント

C#/。NETで、拡張ポイントを提供したいクラスがあります。継承を使用してこれを行うことができます:

public class Animal {
    public virtual void Speak() { }
}
public class Dog : Animal {
    public overrides void Speak() => Console.WriteLine("Woof");
}
var dog = new Dog();
dog.Speak();

または、渡されたデリゲートを使用する:

public class Animal {
    private Action speak;
    public Animal(Action speak) => this.speak = speak;
    public void Speak() => speak();
}
var dog = new Animal(() => Console.WriteLine("Woof"));
dog.Speak();

私はすでにそれらのいくつかの違いを見ることができます:

  • 基本動作へのアクセス-継承による場合、オーバーライドするメソッドは、基本メソッドを呼び出すかどうかを選択できます。デリゲート経由の場合、基本動作への自動アクセスはありません。
  • 動作がありませんか?-継承を介した場合、Speakには常に、基本クラスの動作または派生クラスの動作のいずれかの動作があります。デリゲートを使用する場合、デリゲートフィールドにはnullが含まれる可能性があります(ただし、null許容の参照型の場合、これは発生しません)。
  • スコープデータ/メンバーの明示的な定義-継承を介して拡張する場合、派生クラスで定義された他のメンバーまたはデータは、クラスの一部として明示的に定義されます。デリゲートをラムダ式と一緒に使用すると、ラムダ式は周囲のスコープにアクセスできますが、そのスコープの部分は必ずしもそのように明示的に定義されているわけではありません(たとえば、閉じた変数)。

継承を介して拡張ポイントを公開するのが適切な場合、およびデリゲートを使用するのが適切な場合

3
Zev Spitz

注意:以下は、大まかな「経験則」のみを示しています。これは、継承を正しく使用する方法についての完全なチュートリアルではありませんが、最初の「リトマステスト」として使用できます。あなたが求めていたもののために。

振る舞い拡張ポイントを作成するために、人気のある古典的なアプローチは 戦略パターン です。パターンを完全に見ると、両方にパターンがあります。

  • デリゲートのようにコンテキストクラスに挿入される抽象的な動作(戦略)。 「動物」の例では、これはISpeakStrategyまたはDogSpeakStrategyのような派生を持つインターフェースまたは抽象基本クラスCatSpeakStrategyである可能性があります。ここで、ISpeakStrategyオブジェクトはコンテキストオブジェクトAnimalに挿入されます。

  • 既存のコードに手を加えることなく、使用可能な一連の戦略の拡張を可能にする継承。

これで、拡張ポイントが必要になる場合がありますが、戦略パターンをこのように使用すると、必要以上に複雑になります。

  1. 戦略インターフェースが単一のメソッドのみを必要とし、インターフェースの名前がそれほど重要ではない場合、通常、本格的な継承階層の代わりに単一のデリゲートを使用することで十分です。

  2. 「コンテキスト」オブジェクトと戦略の分離が不要な場合、コンテキストクラスは取るに足らない/ほとんど空になるため、スタンドアロンの継承階層を使用するだけで十分です。

したがって、拡張パターンが戦略パターンでどのように見えるかを考え、設計を複雑にしすぎるものはすべて除外することを検討してください。

5
Doc Brown

「デリゲート」メソッド(例:Action)を使用すると、「注入された」Actionを距離から操作できるため、より高い柔軟性が得られます。 。これにより、完全に分離された動作が提供されるため、インスタンス化後にクラスインスタンスに再び会うことなく、インスタンスの動作を「微調整」できます。

したがって、次のようなことができます。

//Application Root
BarkOptions barkOptions = new BarkOptions()
{
    BarkVolume = 40;
}

Action barking = () =>
{
   Console.WriteLine("Woofed at " + barkOptions.BarkVolume + " dB");

   //BarkVolume is a variable that is controlled by a distant "options" object,
   //which may be available, for example, through some Options panel in your
   //application, for the user to control. Therefore, between each call to
   //wolf.Bark(), the volume may have been changed, thus providing more control.
}

Animal wolf = new Animal(barking);

//Then, the two objects take their separate ways down your object graph.

はい、これは強力ですが、時には、これがあなたが望むものではないかもしれないことを覚えておいてください。これがあなたが望むものではなく、単にクラス内で振る舞いを完全にカプセル化したい場合は、単にメソッドを作成してそれらをオーバーライドすることを明確に見ています派生クラス。

それ以上に、do真剣に Doc Brownの答え を真剣に受け止め、提案するオプションよりも優れたオプションがあります。

3
Vector Zita

クラスを作成するときは、まずYAGNIの原則に従います。拡張性を計画しないでください。あなたはそれを必要としないからです。現時点では、その拡張性がどのように機能するかについての手がかりはありません。

クラスを拡張する必要があると、何が必要になるかがわかります。通常、最善の方法は、ブール値、その他の単純な値、デリゲート、または単なるクロージャであるプロパティを追加することです。次に、そのプロパティに適切に反応するようにクラスを変更します。プロパティが設定されていない場合にどうなるかをドキュメントに追加します(例:背景のColorプロパティが設定されていない場合、「白」が使用されます)。

継承を使用すると、サブクラスを変更せずにクラスを変更する自由が失われ、デリゲートまたはクロージャを使用すると、周囲のコードなしで2つのインスタンス間の違いに明確に対処できます。

0
gnasher729