web-dev-qa-db-ja.com

C#では、インターフェイスメソッドで動作のカップリングを強制する方法はありますか、それとも私がデザインの匂いをしようとしているのですか?

多くの場合、それらの間の動作関係を維持するいくつかのメソッドを持つインターフェースを定義したいと思います。

しかし、多くの場合、この関係は暗黙的なものだと感じています。それを念頭に置いて、私は自分自身に尋ねました:インターフェイスメソッド間の動作関係を強制する方法はありますか?

この動作を継承を介して定義することを考えました(共通の実装を定義することによって)。しかし、C#では多重継承が許可されていないため、多くの場合、インターフェイスの方が賢明であり、継承には十分な柔軟性がないと思います。


例えば:

_public interface IComponent
{
    void Enable();
    void Disable();
    bool IsEnabled();
}
_

このインターフェースでは、次の関係が満たされるようにしました。

  • Enable()が呼び出された場合、IsEnabled()trueを返します。
  • Disable()が呼び出された場合、IsEnabled()falseを返します。

その例では、私が適用したい動作の制約は次のとおりです:

  • Enable()を実装する場合、実装者はIsEnabled()trueを返すことを確認する必要があります
  • Disable()を実装する場合、実装者はIsEnabled()falseを返すことを確認する必要があります

この実装の制約を強制する方法はありますか?または、この種の制約を適用することを検討しているという事実自体が、デザインに欠陥があることを示していますか?

27
Albuquerque

さて、まず、インターフェイスを少し調整しましょう。

_public interface IComponent
{
    void Enable();
    void Disable();
    bool IsEnabled { get; }
}
_

さて。ここで何が問題になる可能性がありますか?たとえば、Enable()またはDisable()メソッドで例外がスローされる可能性はありますか?その場合、IsEnabledはどのような状態になりますか?

コードコントラクトを使用している場合でも、IsEnabledが、EnableメソッドまたはDisableメソッドの使用にどのように関連付けられるかは、これらのメソッドが成功することが保証されていない限りわかりません。 IsEnabledは、架空の状態ではなく、オブジェクトのactual状態を表す必要があります。


つまり、本当に必要なのは

_public interface IComponent
{
    bool IsEnabled { get; set; }
}
_

それをクリアすると、コンポーネントは自身を無効にします。

33
Robert Harvey

あなたが探しているのは Design by Contract と呼ばれるよく知られたアプローチです。 バージョン4.0のフレームワークで直接サポートされています

開示:2019年に新しいプロジェクトにコードコントラクトを追加するときは注意してください。Microsoftによる今後のメンテナンスの現在のステータスは完全には明確ではありません このSO投稿を参照 =、おそらく人気がないためです。

DBCでは、関数とインターフェイスの前提条件、事後条件、および不変条件を指定できるので、Enabled()の呼び出し後にIsEnabledをtrueに強制するコントラクトを記述するだけで済みます。

他の回答が指摘したように、これらの制約が不要な代替設計があるかもしれませんが、この例のために、これらの要件が何らかの理由で正当化されていると仮定しましょう。次に、サンプルインターフェイスの Robert Harveyのバリアント でコードコントラクトを使用すると、次のようになります。

using System.Diagnostics.Contracts;

[ContractClass(typeof(ComponentContract))]
public interface IComponent
{
    void Enable();
    void Disable();
    bool IsEnabled { get; } 
}

[ContractClassFor(typeof(IComponent))]
sealed class ComponentContract : IComponent
{
    [Pure]
    public bool IsEnabled => Contract.Result<bool>();

    public void Disable()
    {
        Contract.Ensures(IsEnabled == false);
    }

    public void Enable()
    {
        Contract.Ensures(IsEnabled == true);
    }
}

コードコントラクトの簡単なチュートリアルについては、 こちら を参照してください。

また、この質問の 私の2番目の回答 も確認してください。これは、将来廃止される可能性があるライブラリに依存しない解決策を提供します。

39
Doc Brown

あまりにも多くのC#インターフェイスを要求しています。

C#インターフェイスはコントラクトです。彼らは与えられたクラスが実装するメソッドのパックを言っており、誰かがそれらを呼び出した場合にそれらのメソッドがそこにあることを保証します

そうは言っても、それはC#インターフェースが行う唯一のことでもあります。

それらは、実装されたメソッドが何をするのかを完全に知らない。彼らは好きなことをするのは自由です。

特定のクラスは、常にisEnabledを実装してtrueを返すことができます。もう1つは、 "無効化"をデータベース呼び出しに結び付け、特定のことが起こった場合に無効化を拒否することができます。

あなたはそれを制御することはできません。それはあなたのC#インターフェースの仕事ではありません。


この動作を強制するにはどうすればよいですか?

テストを使用します。

問題のタイプのオブジェクトを受け入れることができる単体テストのグループを作成し、動作をテストします。

テストに合格したら、問題ありません。それらが失敗した場合、何かがおかしいので、コードをチェックする必要があります。

とはいえ、APIを開発している場合、これをサードパーティに強制するエレガントな方法はありません。それはC#インターフェイスの目的ではありません。

32
T. Sar

状態遷移は、状態ごとに個別のインターフェースで表すことができます。

public interface IEnabledComponent
{
    IDisabledComponent ToDisabled();
}

public interface IDisabledComponent
{
    IEnabledComponent ToEnabled();
}

現在の状態に応じてさまざまなメソッドを公開できるため、これははるかに強力で安全です。特に、IsEnabledプロパティも必要ありません。

または、単一の本当にシンプルなインターフェースを使用することもできます。

public interface IComponent
{
    bool IsEnabled {get;set;}
}

ここでは、状態は単一のメンバーで表されるため、複数の関連するメンバーを調整する必要はありません。

異なる状態が異なる動作を引き起こす場合は最初のオプションを選択し、有効状態が他の動作に影響を与えない場合は2番目のオプションを選択します。

インターフェイスの異なるメンバー間の動作関係を定義できるという事実は、それらが同じ情報を表現しているため冗長であることを示しています。より簡単な例を挙げましょう:

interface IComponent 
{
  bool IsEnabled {get;set;}
  bool IsDisabled {get;set;}
}

ここで、IsEnabledがtrueの場合、IsDisabledがfalseであるという関係を定義できます。しかし、これは、独立した情報や動作を表していないため、そのうちの1つを削除できることを示しています。

21
JacquesB

使用パターンを「伝統的」に見せたい場合のもう1つのオプション* ただし、インターフェースを制限できるので、メソッドを結合する必要はありません。拡張メソッドを使用して、不足しているメソッドを「追加」できます。

あなたの例では、インターフェースはIsEnabled {get;set;}およびEnableおよびDisableは、インターフェイスを定義するライブラリの一部として提供される拡張機能です。

public interface IComponent
{
    bool IsEnabled {get;set;}
}

public static class IComponentExtensions
{
    public static void Enable(this IComponent component)
    {
         component.IsEnabled = true;
    } 
    public static void Disable(this IComponent component)
    {
         component.IsEnabled = false;
    } 
}

インスタンスメソッドは拡張よりも優先され、具象クラスは同じシグネチャを持つメソッドを実装して、コンパイラにクラス固有の実装を選択させるため、混乱を招く可能性があることに注意してください。一方、ほとんどすべての人がLINQでこのパターンの重要な経験を持っています。そのため、インターフェースを絞り込んでそれを許可できるかどうか検討する価値があります。


* フレームワークのStreamクラスなど、特定のタイプにいくつかの特定のメソッドがあることを期待している人々の意味での「従来の」インターフェース-通常のストリームが「新規」/ "廃棄"。

5
Alexei Levenkov

my other answer に示されているアプローチとはまったく異なるアプローチを使用してこれを解決する別のアイデアを次に示します(したがって、私はそれを個別に投稿します):テンプレートメソッドパターンを利用します。繰り返しますが、例として、IsEnabledを機能させるための要件が​​何らかの理由で正当化され、その状態が正しく更新されていることを確認したいとします。

次に、インターフェイスを抽象クラスに置き換え、IsEnabledを(例外の場合でも)無条件に切り替えられるブールプロパティにして、その抽象クラスのユーザーに、元のクラスの代わりに2つのテンプレートメソッドを実装させることができます。もの:

public abstract class Component  // replacement for IComponent 
{
    public bool IsEnabled{get;private set;}

    public void Enable()
    {
       try
       {
          EnableImpl();
       }
       finally
       {
          IsEnabled=true;
       }
    }
    public void Disable()
    {
       try
       {
          DisableImpl();
       }
       finally
       {
          IsEnabled=false;
       }
    }

    protected virtual void EnableImpl();
    protected virtual void DisableImpl();
}

現在、ユーザーはEnableImplおよびDisableImplを独自の実装でオーバーライドできますが、状態の追跡は確実に行われます。

このアプローチは、私の最初の提案よりも明らかに標準的であり、Microsoftによってすぐに非推奨になるとは思わないでしょう。

3
Doc Brown

別の解決策(私はnotを一般的に推奨しますが、あなたが求めていることを達成します)はこれです。

public class Switch //make sealed if you really want to enforce behaviour
{  

    public bool IsEnabled {get; private set;}

    public void Enable() //make virtual if you don't want to enforce this behaviour
    { 
        IsEnabled = true;
    }

    public void Disable() //make virtual if you don't want to enforce this behaviour
    {
        IsEnabled = false;
    }
}

public interface IComponent {
    Switch Switch{get;}
}
0
Guran