接口或类的依赖注入 [英] Dependency injection with interfaces or classes

查看:29
本文介绍了接口或类的依赖注入的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

I've been guilty of having a 1-to-1 relationship between my interfaces and concrete classes when using dependency injection. When I need to add a method to an interface, I end up breaking all the classes that implement the interface.

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

This is a simple example, but let's assume that I need to inject an ILogger into one of my classes.

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

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

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

Having a 1-to-1 relationship like this feels like a code smell. Since I only have a single implementation, are there any potentially issues if I create a class and mark the Info method as virtual to override in my tests instead of having to create an interface just for a single class?

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

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

If I needed another implementation, I can override the Info method:

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

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

If each of these classes have specific properties or methods that would create a leaky abstraction, I could extract out a base class:

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>.

The reason why I didn't mark the base class as abstract is because if I ever wanted to add another method, I wouldn't break existing implementations. For example, if my FileLogger needed a Debug method, I can update the base class Logger without breaking the existing 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) { }
}

同样,这是一个简单的例子,但我什么时候应该更喜欢界面?

Again, this is a simple example, but when I should I prefer an interface?

推荐答案

快速"答案

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

I would stick with interfaces. They are designed to be contracts for consumption for external entities.

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

@JakubKonecki mentioned multiple inheritance. I think this is the biggest reason to stick with interfaces as it will become very apparent on the consumer side if you force them to take a base class... no one likes base classes being thrust upon them.

更新后的快速"答案

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

You have stated issues with interface implementations outside your control. A good approach is to simply create a new interface inheriting from the old one and fix your own implementation. You can then notify the other teams that a new interface is available. Over time, you can deprecate older interfaces.

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

Don't forget you can use the support of explicit interface implementations to help maintain a nice divide between interfaces that are logically the same, but of different versions.

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

If you want all this to fit in with DI, then try not to define new interfaces and instead favour additions. Alternatively to limit client code changes, try to inherit new interfaces from old ones.

实施与消费

实现接口和使用接口是有区别的.添加方法会破坏实现,但不会破坏使用者.

There is a difference between implementing the interface and consuming it. Adding a method breaks the implementation(s), but does not break the consumer.

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

Removing a method obviously breaks the consumer, but does not break the implementation - however you wouldn't do this if you are backwards-compatibility conscious for your consumers.

我的经历

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

We frequently have a 1-to-1 relationship with interfaces. It is largely a formality but you occasionally get nice instances where interfaces are useful because we stub / mock test implementations, or we actually provide client-specific implementations. The fact that this frequently breaks that one implementation if we happen to change the interface isn't a code smell, in my opinion, it is simply how you work against interfaces.

我们基于接口的方法现在使我们处于有利地位,因为我们利用工厂模式和 DI 元素等技术来改进过时的遗留代码库.测试能够快速利用这样一个事实,即接口在找到最终"用途(即,不仅仅是具有具体类的 1-1 映射)之前在代码库中存在多年.

Our interface-based approach is now standing us in good stead as we utilise techniques such as the factory pattern and elements of DI to improve an aged legacy code base. Testing was able to quickly take advantage of the fact that interfaces existed in the code base for many years before finding a "definitive" use (ie, not just 1-1 mappings with concrete classes).

基类缺点

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

Base classes are for sharing implementation details to common entities, the fact they are able to do something similar with sharing an API publicly is a by-product in my opinion. Interfaces are designed to share API publicly, so use them.

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

With base classes you could also potentially get leakage of implementation details, for example if you need to make something public for another part of the implementation to use. These are no conducive to maintaining a clean public API.

突破性/支持性实施

如果您沿着接口路线走下去,您可能会因为违反合同而难以更改接口.此外,正如您所提到的,您可能会破坏您无法控制的实现.有两种方法可以解决这个问题:

If you go down the interface route you may run into difficulty changing even the interface due to breaking contracts. Also, as you mention, you could break implementations outside of your control. There are two ways to tackle this problem:

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

我目睹了后者,我认为它有两种形式:

I have witnessed the latter, I see it come in two guises:

  1. 针对任何新内容完全独立的界面:MyInterfaceV1MyInterfaceV2.
  2. 接口继承:MyInterfaceV2 : MyInterfaceV1.

我个人不会选择沿着这条路走下去,我会选择不支持实现中断更改.但有时我们没有这个选择.

I personally wouldn't choose to go down this route, I would choose to not support implementations from breaking changes. But sometimes we don't have this choice.

一些代码

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

这篇关于接口或类的依赖注入的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆