41

依存性注入を使用するときに、インターフェースと具象クラスの間に1対1の関係があることで罪を犯してきました。インターフェイスにメソッドを追加する必要がある場合、インターフェイスを実装するすべてのクラスを壊してしまいます。

ILoggerこれは単純な例ですが、クラスの1つにを注入する必要があると仮定しましょう。

public interface ILogger
{
    void Info(string message);
}

public class Logger : ILogger
{
    public void Info(string message) { }
}

このような1対1の関係を持つことは、コードの臭いのように感じます。実装は1つしかないのでInfo、単一のクラス専用のインターフェイスを作成する代わりに、クラスを作成してメソッドを仮想としてマークし、テストでオーバーライドする場合、潜在的な問題はありますか?

public class Logger
{
    public virtual void Info(string message)
    {
        // Log to file
    }
}

別の実装が必要な場合は、Infoメソッドをオーバーライドできます。

public class SqlLogger : Logger
{
    public override void Info(string message)
    {
        // Log to SQL
    }
}

これらの各クラスに、リークのある抽象化を作成する特定のプロパティまたはメソッドがある場合、基本クラスを抽出できます。

public class Logger
{
    public virtual void Info(string message)
    {
        throw new NotImplementedException();
    }
}

public class SqlLogger : Logger
{
    public override void Info(string message) { }
}

public class FileLogger : Logger
{
    public override void Info(string message) { }
}

基本クラスを抽象としてマークしなかった理由は、別のメソッドを追加したい場合でも、既存の実装を壊さないためです。たとえば、メソッドFileLoggerが必要な場合は、既存のを壊さずDebugに基本クラスを更新できます。LoggerSqlLogger

public class Logger
{
    public virtual void Info(string message)
    {
        throw new NotImplementedException();
    }

    public virtual void Debug(string message)
    {
        throw new NotImplementedException();
    }
}

public class SqlLogger : Logger
{
    public override void Info(string message) { }
}

public class FileLogger : Logger
{
    public override void Info(string message) { }
    public override void Debug(string message) { }
}

繰り返しますが、これは単純な例ですが、いつインターフェイスを使用する必要がありますか?

4

4 に答える 4

33

「クイック」回答

私はインターフェースに固執します。それらは、外部エンティティの消費の契約となるように設計されています。

@JakubKoneckiは多重継承について言及しました。これがインターフェースに固執する最大の理由だと思います。基本クラスを強制的に取得すると、消費者側で非常に明らかになるからです...基本クラスがそれらに押し付けられるのを好む人は誰もいません。

更新された「クイック」回答

あなたは、あなたのコントロールの外にあるインターフェースの実装に関する問題を述べました。良いアプローチは、古いインターフェースを継承する新しいインターフェースを作成し、独自の実装を修正することです。その後、新しいインターフェースが利用可能であることを他のチームに通知できます。時間の経過とともに、古いインターフェースを廃止することができます。

明示的なインターフェース実装のサポートを使用して、論理的には同じであるがバージョンが異なるインターフェース間の適切な分割を維持できることを忘れないでください。

これらすべてをDIに適合させたい場合は、新しいインターフェースを定義せず、代わりに追加を優先するようにしてください。または、クライアントコードの変更を制限するには、古いインターフェイスから新しいインターフェイスを継承してみてください。

実装と消費

インターフェイスの実装と使用には違いがあります。メソッドを追加すると、実装は中断されますが、コンシューマーは中断されません。

メソッドを削除すると、明らかにコンシューマーが破損しますが、実装は破損しません。ただし、下位互換性を意識している場合は、これを実行しないでください。

私の経験

私たちはしばしばインターフェースと1対1の関係を持っています。これは主に形式的なものですが、テストの実装をスタブ/モックしたり、実際にクライアント固有の実装を提供したりするため、インターフェイスが役立つ場合があります。私の意見では、これがインターフェイスを変更した場合に1つの実装を頻繁に壊すという事実はコードの臭いではなく、単にインターフェイスに対してどのように作業するかです。

ファクトリパターンやDIの要素などの手法を利用して、古くなったレガシーコードベースを改善しているため、インターフェイスベースのアプローチは今や私たちをしっかりと支えています。テストでは、「決定的な」使用法(つまり、具象クラスとの1対1のマッピングだけでなく)を見つける前に、インターフェースがコードベースに長年存在していたという事実をすばやく利用できました。

基本クラスの短所

基本クラスは、実装の詳細を一般的なエンティティと共有するためのものです。APIを公開して共有するのと同様のことができるという事実は、私の意見では副産物です。インターフェースはAPIを公に共有するように設計されているので、それらを使用してください。

基本クラスを使用すると、実装の別の部分で使用するために何かを公開する必要がある場合など、実装の詳細が漏洩する可能性もあります。これらは、クリーンなパブリックAPIを維持するのに役立ちません。

実装の中断/サポート

インターフェイスルートをたどると、契約が破られてインターフェイスを変更するのが困難になる可能性があります。また、あなたが言及するように、あなたはあなたのコントロールの外で実装を壊す可能性があります。この問題に取り組むには2つの方法があります。

  1. 消費者を壊すことはありませんが、実装をサポートしないことを述べてください。
  2. インターフェースが公開されると、変更されることはないことを述べます。

私は後者を目撃しました、私はそれが2つの形で来るのを見ます:

  1. 新しいもののために完全に分離されたインターフェース:MyInterfaceV1MyInterfaceV2
  2. インターフェイスの継承:MyInterfaceV2 : MyInterfaceV1

私は個人的にこのルートを選択することはしませんでした。変更を壊すことによる実装をサポートしないことを選択しました。しかし、この選択肢がない場合もあります。

いくつかのコード

public interface IGetNames
{
    List<string> GetNames();
}

// One option is to redefine the entire interface and use 
// explicit interface implementations in your concrete classes.
public interface IGetMoreNames
{
    List<string> GetNames();
    List<string> GetMoreNames();
}

// Another option is to inherit.
public interface IGetMoreNames : IGetNames
{
    List<string> GetMoreNames();
}

// A final option is to only define new stuff.
public interface IGetMoreNames 
{
    List<string> GetMoreNames();
}
于 2012-04-25T08:00:48.400 に答える
12

、、、およびのほかにメソッドを追加し始めると、インターフェイスはインターフェイス分離の原則ILoggerを破っています。恐ろしいLog4NetILogインターフェースを見てください。そうすれば、私が何について話しているのかがわかります。DebugErrorCriticalInfo

ログの重大度ごとにメソッドを作成する代わりに、ログオブジェクトを受け取る単一のメソッドを作成します。

void Log(LogEntry entry);

これにより、すべての問題が完全に解決されます。理由は次のとおりです。

  1. LogEntry単純なDTOになり、クライアントを壊すことなく、新しいプロパティを追加できます。
  2. ILoggerその単一のメソッドにマップするインターフェイスの拡張メソッドのセットを作成できますLog

このような拡張メソッドの例を次に示します。

public static class LoggerExtensions
{
    public static void Debug(this ILogger logger, string message)
    {
        logger.Log(new LogEntry(message)
        {
            Severity = LoggingSeverity.Debug,
        });
    }

    public static void Info(this ILogger logger, string message)
    {
        logger.Log(new LogEntry(message)
        {
            Severity = LoggingSeverity.Information,
        });
    }
}

この設計の詳細については、こちらをお読みください

于 2012-04-25T11:15:16.107 に答える
4

あなたは常にインターフェースを好むべきです。

はい、場合によっては、クラスとインターフェイスで同じメソッドを使用しますが、より複雑なシナリオでは使用できません。また、.NETには多重継承がないことを忘れないでください。

インターフェイスは別のアセンブリに保持し、クラスは内部にある必要があります。

インターフェイスに対するコーディングのもう1つの利点は、単体テストでインターフェイスを簡単にモックできることです。

于 2012-04-25T07:57:32.433 に答える
0

私はインターフェースを好みます。スタブとモックも実装(一種)であることを考えると、私は常に任意のインターフェースの少なくとも2つの実装を持っています。また、インターフェイスはテスト用にスタブ化およびモック化できます。

さらに、アダム・ホルズワースが言及する契約角度は非常に建設的です。私見では、インターフェースの1-1実装よりもコードがクリーンになり、臭いがします。

于 2012-04-25T08:28:47.653 に答える