2012-04-25 3 views
24

私は、依存関係注入を使用しているときに、自分のインタフェースと具象クラスとの間に1対1の関係があることを認めています。インターフェイスにメソッドを追加する必要があるとき、私はインターフェイスを実装するすべてのクラスを壊してしまいます。インタフェースまたはクラスによる依存注入

これは簡単な例ですが、自分のクラスの1つに​​を注入する必要があるとしましょう。

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

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

このような1対1の関係を持つことは、コードの匂いのように感じます。クラスを作成し、単一のクラスだけのインターフェイスを作成するのではなく、テストでオーバーライドするためにInfoメソッドを仮想としてマークすると、1つの実装しかないので、潜在的な問題はありますか?これらの各クラスが漏れやすい抽象化を作成することになり、特定のプロパティやメソッドを持っている場合は

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

が、私はベースを抽出することができます:私は別の実装を必要と

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

場合、私はInfoメソッドをオーバーライドすることができますクラス:

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) { } 
} 

私が今まで別のメソッドを追加したい場合は、私はexisを破るないので、私は抽象として基本クラスをマークしなかった理由は、実装例です。たとえば、FileLoggerDebugメソッドが必要な場合は、既存のSqlLoggerを破損することなく、ベースクラスLoggerを更新できます。

また、これは簡単な例ですが、私はインターフェイスを好むべきですか?

+1

_私が抽象クラスとして基底クラスをマークしなかったのは、別のメソッド_Hmを追加したければ抽象クラスに実装を含めることができるからです。 Debugメソッドを抽象Loggerクラスに追加することができます。 –

+0

既存の実装を破ることは、再利用可能なライブラリを作成する場合にのみ問題になります。あなたですか?あるいは単にビジネスアプリケーションのラインを書いていますか? – Steven

+0

それは問題の範囲ではありませんが、継承は過大評価されています。 'SqlLogger'は' SqlLogPersistenceStrategy'を使った具体的な 'Logger'です。ほとんどの場合、コンポジションは継承よりもはるかに優れています。あなたの問題についても、ISPはどうですか? 'ILogInfo'、' ILogError'など – plalx

答えて

27

"クイック" 私はインタフェースに固執するだろう

回答。彼らは外部エンティティのための消費のために設計されています。

@JakubKoneckiは多重継承を述べました。私はこれがインターフェースに固執する最大の理由だと思います。あなたが基本クラスを取るように強制すると、それは消費者側で非常に明白になります。

更新「クイック」

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

explicit interface implementationsのサポートを使用すると、論理的には同じですが異なるバージョンのインターフェイス間で良好な分割を維持できます。

これをすべてDIに収めるには、新しいインターフェイスを定義せず、代わりに追加することをお勧めします。あるいは、クライアントコードの変更を制限するために、古いものから新しいインタフェースを継承してみてください。消費

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

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

私の経験

私たちは頻繁にインタフェースを備えた1対1の関係を有しています。これは主に形式ですが、私たちがテスト実装をスタブ/モックするか、実際にはクライアント固有の実装を提供しているため、インタフェースが便利な良いインスタンスが得られることがあります。私たちがインタフェースを変更した場合、これが頻繁に1つの実装を壊すという事実は、コードの匂いではありません。

私たちのインターフェイスベースのアプローチは、古いパターンのコードベースを改善するために、工場パターンやDIの要素などの技術を利用するので、今のところ優れた立場に立っています。テストでは、インタフェースがコードベースに長年存在していたことを利用して、「決定的な」使用(すなわち、具象クラスによる1-1のマッピングだけでなく)を見つけることができました。

基本クラスの短所

基本クラスは、共通のエンティティに実装の詳細を共有するためのもので、彼らは公にAPIを共有して類似した何かをすることができます事実は、私の意見では、副生成物です。インターフェイスは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(); 
} 
+0

私が抱えている困難の1つは、このインターフェイスが企業全体で共有されている場合です。インターフェイスを更新すると、このインターフェイスを使用する他のすべてのチームの実装が中断されます。この時点で、私たちはこれらの変更を強制しています。完璧な世界では、誰もが喜んでその実装を更新するつもりですが、それは必ずしもそうではありません。 – nivlam

+0

インタフェースを変更できない場合、別のものを作成することができます: 'inteface IDebuger {void Debug(string message);}'とFileLoggerで実装します。したがって、他のチームがそれを必要としなければ、それを使用して実装することはありません。 –

+0

@nivlamこれに代わる方法は、インターフェイスが作成された後、変更するためにロックされます。古いインタフェースを継承する新しいインタフェースを作成し、必要に応じて新しいインタフェースを実装することもできます。つまり、内部実装のみを意味します。私は自分の答えを更新しました。 –

2

常にインターフェイスを使用することをお勧めします。

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

インターフェイスは別のアセンブリに保存し、クラスは内部にしてください。

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

+0

インターフェイスを別のアセンブリに保つのはなぜ良いことですか? – thedev

+4

@thedev実装を公開せずにインターフェイスを公開することができます。これらは、オーバーヘッドやメンテナンスの問題を追加するだけの不要な実装を漏らさずに共有することもできます。 –

0

私はインターフェイスを好みます。与えられたスタブとモックがインプリメンテーション(ある種の実装)であるとすれば、私はいつも少なくとも2つのインタフェースを実装しています。また、テストのためにインターフェイスをスタブしたり、モックすることもできます。

さらに、アダム・ホーズワース氏が言及している契約角度は非常に建設的です。 IMHOはインターフェイスの1-1実装よりもコードをきれいにして、それを臭いにします。あなたはInfo以外DebugError、およびCriticalメソッドの追加を開始するとき

9

あなた​​インタフェースはinterface segregation principleを壊しています。 horrible Log4Net ILog interfaceを見ると、私が何を言っているのか分かります。代わりに、ログの重要度ごとにメソッドを作成する

、ログオブジェクトを受け取り、単一の方法で作成します。

  1. LogEntryは次のようになりますので、これは完全に、あなたの問題のすべてを解決

    void Log(LogEntry entry); 
    

    を単純なDTOであり、クライアントを破棄することなく新しいプロパティを追加することができます。

  2. 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, 
         }); 
        } 
    } 
    

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

はここで、このような拡張メソッドの例です。

+0

問題は今、契約でサポートされている重大度レベルが定義されていないことです。 ISPを念頭に置いて、 'ILogInfo'、' ILogError'、 'ILogDebug'などはどうでしょうか? – plalx

関連する問題