C# 接口或类的依赖注入

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/10311571/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-09 13:18:24  来源:igfitidea点击:

Dependency injection with interfaces or classes

c#dependency-injection

提问by nivlam

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.

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

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

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

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

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

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 Infomethod as virtual to override in my tests instead of having to create an interface just for a single class?

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

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

If I needed another implementation, I can override the Infomethod:

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

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

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 FileLoggerneeded a Debugmethod, I can update the base class Loggerwithout breaking the existing SqlLogger.

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

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

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

采纳答案by Adam Houldsworth

The "Quick" Answer

“快速”答案

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

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

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

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

The Updated "Quick" Answer

更新的“快速”答案

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 implementationsto help maintain a nice divide between interfaces that are logically the same, but of different versions.

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

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.

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

Implementation vs. Consumption

实施与消费

There is a difference between implementingthe interface and consumingit. 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.

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

My Experiences

我的经历

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.

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

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

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

Base Class Cons

基类缺点

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 类似的事情是副产品。接口旨在公开共享 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.

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

Breaking / Supporting Implementations

打破/支持实现

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. State that you won't break consumers, but you won't support implementations.
  2. State that once an interface is published, it is never changed.
  1. 声明你不会破坏消费者,但你不会支持实现。
  2. 声明接口一旦发布,就永远不会改变。

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

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

  1. Completely separate interfaces for any new stuff: MyInterfaceV1, MyInterfaceV2.
  2. Interface inheritance: MyInterfaceV2 : MyInterfaceV1.
  1. 任何新东西的完全独立的接口:MyInterfaceV1, MyInterfaceV2.
  2. 接口继承:MyInterfaceV2 : MyInterfaceV1.

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

我个人不会选择走这条路,我会选择不支持破坏性更改的实现。但有时我们没有这个选择。

Some Code

一些代码

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

回答by Jakub Konecki

You should always prefer the interface.

您应该始终更喜欢界面。

Yes, in some cases you will have the same methods on class and interface, but in more complex scenarios you won't. Also remember, that there is no multiple inheritance in .NET.

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

You should keep your interfaces in a separate assembly and your classes should be internal.

你应该把你的接口放在一个单独的程序集中,你的类应该是内部的。

Another benefit of coding against interfaces is an ability to easily mock them in unit tests.

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

回答by Morten

I Prefer interfaces. Given stubs and mocks are also implementations (sort of), I always have at least two implementations of any interface. Also, Interfaces can be stubbed and mocked for tests.

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

Further, the contract angle that Adam Houldsworth mentions is very constructive. IMHO it makes the code cleaner than 1-1 implementations of interfaces make it smelly.

此外,Adam Houldsworth 提到的合同角度非常具有建设性。恕我直言,它使代码比接口的 1-1 实现更干净,使其发臭。

回答by Steven

Your ILoggerinterface is breaking the interface segregation principlewhen you start adding Debug, Error, and Criticalmethods besides Info. Take a look at the horrible Log4Net ILog interfaceand you'll know what I'm talking about.

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

Instead of creating a method per log severity, create a single method that takes a log object:

不是为每个日志严重性创建一个方法,而是创建一个接受日志对象的方法:

void Log(LogEntry entry);

This completely solves all of your problems, because:

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

  1. LogEntrywill be a simple DTO and you can add new properties to it, without breaking any client.
  2. You can create a set of extension methods for your ILoggerinterface that map to that single Logmethod.
  1. LogEntry将是一个简单的 DTO,您可以向其添加新属性,而不会破坏任何客户端。
  2. 您可以为您的ILogger接口创建一组映射到该单个Log方法的扩展方法。

Here's an example of such extension method:

这是此类扩展方法的示例:

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

For a more detailed discussion on this design, please read this.

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