使用接口或类进行依赖注入

时间:2012-04-25 07:54:54

标签: c# dependency-injection

在使用依赖注入时,我在界面和具体类之间建立了一对一的关系。当我需要向接口添加方法时,我最终会破坏实现该接口的所有类。

这是一个简单的例子,但我们假设我需要在其中一个类中注入ILogger

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

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

像这样的一对一关系感觉就像代码味道。由于我只有一个实现,如果我创建一个类并将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方法,我可以在不破坏现有Logger的情况下更新基类SqlLogger

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 个答案:

答案 0 :(得分:31)

“快速”答案

我会坚持使用接口。它们设计是外部实体的消费合同。

@JakubKonecki提到了多重继承。我认为这是坚持使用界面的最大理由,因为如果你强迫他们选择基类,它将在消费者方面变得非常明显......没有人喜欢基类被强加给他们。

更新后的“快速”答案

您已经说明了控件之外的接口实现问题。一个好的方法是简单地创建一个继承旧的接口并修复自己的实现。然后,您可以通知其他团队新的界面可用。随着时间的推移,您可以弃用旧接口。

不要忘记你可以使用explicit interface implementations的支持来帮助在逻辑上相同但不同版本的接口之间保持良好的划分。

如果您希望所有这些都适合DI,那么尽量不要定义新的接口,而是支持添加。或者,如果要限制客户端代码更改,请尝试从旧接口继承新接口。

实施与消费

实施界面与消费之间存在差异。添加方法会破坏实现,但不会破坏消费者。

删除方法显然会破坏消费者,但不会破坏实施 - 但如果您对消费者具有向后兼容性,则不会这样做。

我的经历

我们经常与接口建立一对一的关系。它在很大程度上是一种形式,但你偶尔会得到很好的实例,其中接口是有用的,因为我们存根/模拟测试实现,或者我们实际上提供客户端特定的实现。如果我们碰巧改变界面,这经常打破一个实现的事实不是代码气味,在我看来,它只是你如何对抗接口。

我们基于接口的方法现在正处于有利地位,因为我们利用工厂模式和DI元素等技术来改善老化的遗留代码库。在找到“确定”用法之前,测试能够快速利用代码库中存在接口的事实多年(即,不仅仅是1-1与具体类别的映射)。

基类缺点

基类用于向普通实体共享实现细节,在我看来,他们能够通过公开共享API做类似的事情是副产品。接口旨在公开共享API,因此请使用它们。

对于基类,您还可能会泄漏实现细节,例如,如果您需要为实现的另一部分公开使用某些内容。这些都不利于维护一个干净的公共API。

打破/支持实施

如果你沿着界面路线走下去,由于合同中断,你甚至可能难以改变界面。此外,正如您所提到的,您可能会破坏控件之外的实现。有两种方法可以解决这个问题:

  1. 声明您不会破坏消费者,但您不会支持实施。
  2. 声明一旦发布了接口,它就永远不会被更改。
  3. 我亲眼目睹了后者,我认为它有两种形式:

    1. 完全区分任何新内容的界面:MyInterfaceV1MyInterfaceV2
    2. 接口继承:MyInterfaceV2 : MyInterfaceV1
    3. 我个人不会选择沿着这条路走下去,我会选择不支持破坏变更的实现。但有时我们没有这个选择。

      部分代码

      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();
      }
      

答案 1 :(得分:11)

当您开始添加ILogger之外的DebugErrorCritical方法时,Info界面正在打破interface segregation principle。看看horrible Log4Net ILog interface,你就会知道我在说什么。

不是按日志严重性创建方法,而是创建一个采用日志对象的方法:

void Log(LogEntry entry);

这完全解决了您的所有问题,因为:

  1. LogEntry将是一个简单的DTO,您可以为其添加新属性,而不会破坏任何客户端。
  2. 您可以为映射到该ILogger方法的Log界面创建一组扩展方法。
  3. 以下是此类扩展方法的示例:

    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

答案 2 :(得分:4)

你应该总是喜欢这个界面。

是的,在某些情况下,您将在类和接口上使用相同的方法,但在更复杂的情况下,您不会。还要记住,.NET中没有多重继承。

您应该将接口保存在单独的程序集中,并且您的类应该是内部的。

对接口进行编码的另一个好处是能够在单元测试中轻松模拟它们。

答案 3 :(得分:0)

我喜欢接口。鉴于存根和模拟也是实现(有点),我总是至少有两个任何接口的实现。此外,可以对接口进行存根和模拟以进行测试。

此外,Adam Houldsworth提到的契约角度是非常有建设性的。恕我直言,它使代码更清洁,而不是1-1的接口实现让它变得臭。