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

niv*_*lam 31 c# dependency-injection

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

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

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

public class Logger : ILogger
{
    public void Info(string message) { }
}
Run Code Online (Sandbox Code Playgroud)

像这样的一对一关系感觉就像代码味道.由于我只有一个实现,如果我创建一个类并将该Info方法标记为虚拟以在我的测试中覆盖而不是仅为一个类创建一个接口,是否有任何潜在的问题?

public class Logger
{
    public virtual void Info(string message)
    {
        // Log to file
    }
}
Run Code Online (Sandbox Code Playgroud)

如果我需要另一个实现,我可以覆盖该Info方法:

public class SqlLogger : Logger
{
    public override void Info(string message)
    {
        // Log to SQL
    }
}
Run Code Online (Sandbox Code Playgroud)

如果这些类中的每一个都具有可以创建漏洞抽象的特定属性或方法,我可以提取出一个基类:

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) { }
}
Run Code Online (Sandbox Code Playgroud)

我没有将基类标记为抽象的原因是因为如果我想添加另一个方法,我不会破坏现有的实现.例如,如果我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) { }
}
Run Code Online (Sandbox Code Playgroud)

再一次,这是一个简单的例子,但是当我应该更喜欢一个界面时?

Ada*_*rth 32

"快速"答案

我会坚持使用接口.它们旨在成为外部实体的消费合同.

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

更新的"快速"答案

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

不要忘记您可以使用显式接口实现的支持来帮助在逻辑上相同但不同版本的接口之间保持良好的划分.

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

实施与消费

实现界面和使用它之间存在差异.添加方法会破坏实现,但不会破坏使用者.

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

我的经历

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

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

基类缺点

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

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

打破/支持实施

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

  1. 声明您不会破坏消费者,但您不会支持实施.
  2. 说明一旦界面发布,它就永远不会改变.

我目睹了后者,我看到它有两种形式:

  1. 任何新东西的完全独立的接口:MyInterfaceV1,MyInterfaceV2.
  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();
}
Run Code Online (Sandbox Code Playgroud)


Ste*_*ven 11

你的ILogger界面更是打破了接口隔离原则,当你开始增加Debug,ErrorCritical另外的方法Info.看看可怕的Log4Net ILog界面,你就会知道我在说什么.

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

void Log(LogEntry entry);
Run Code Online (Sandbox Code Playgroud)

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

  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,
        });
    }
}
Run Code Online (Sandbox Code Playgroud)

有关此设计的更详细讨论,请阅读此内容.