依赖注入和特定的依赖实现

时间:2011-04-26 07:37:50

标签: c# .net dependency-injection

首先让我介绍一下没有依赖注入的实现(它会打破依赖倒置原则):

public class MyValidator
{
  private readonly IChecksumGenerator _checksumGenerator;

  public MyValidator()
  {
     _checksumGenerator = new MyChecksumGenerator();
  }

  ...
}

要使此代码可测试,请注入IChecksumGenerator:

public class MyValidator
{
  private readonly IChecksumGenerator _checksumGenerator;

  public MyValidator(IChecksumGenerator checksumGenerator)
  {
    _checksumGenerator = checksumGenerator; 
  }

  ...
}

现在我们可以根据需要轻松测试MyValidator和stub checksumGenerator。但MyValidator实现在算法上耦合到特定的IChecksumGenerator实现(它不适用于任何其他实现)。所以出现了一些问题:

  1. 我们介绍了注入不正确的IChecksumGenerator的可能性(例如,由于IoC容器配置错误)
  2. 我们打破封装,因为MyValidator的私有实现细节(耦合到MyChecksumGenerator)超出了类
  3. 我遇到的最佳解决方案如下:

    public class MyValidator
    {
      private readonly IChecksumGenerator _checksumGenerator;
    
      public MyValidator()
      {
        _checksumGenerator = new MyChecksumGenerator;
      }
    
      internal MyValidator(IChecksumValidator checksumValidator)
      {
        _checksumValidator = checksumValidator;
      }
    
      ...
    }
    

    这里我介绍了用于测试目的的特殊构造函数(因此我可以在测试中存根IChecksumValidator),但是公共构造函数创建它所耦合的实现(因此封装不会被破坏)。为测试目的创建一些代码有点难看,但在这种情况下看起来很有意义。

    你会如何解决这个问题?

5 个答案:

答案 0 :(得分:5)

重构为构造函数注入是一个非常好的主意,但我发现问题中提出的约束很奇怪。我建议你重新考虑设计。

如果MyValidator仅适用于IChecksumGenerator的一个特定实现,则它将违反Liskov Substitution Principle(LSP)。从本质上讲,这也意味着你将无法注入Test Double,因为存根/ mock / fake /不是“正确的”IChecksumGenerator的实例。

从某种意义上说,你可以说API是关于它的要求的,因为它声明它可以处理任何IChecksumGenerator,而实际上它只适用于一种特定类型 - 让我们称之为OneAndOnlyChecksumGenerator。虽然我建议重新设计应用程序以遵守LSP,但您也可以更改构造函数签名以确保该要求:

public class MyValidator
{
    private readonly OneAndOnlyChecksumGenerator checksumGenerator;

    public MyValidator(OneAndOnlyChecksumGenerator checksumGenerator)
    {
        this.checksumGenerator = checksumGenerator; 
    }

    // ...
}

您可能仍然可以通过将战略成员设置为虚拟来将OneAndOnlyChecksumGenerator变为Test Double,以便您可以创建特定于测试的子类。

答案 1 :(得分:3)

这不违反封装。验证通常涉及校验和。

我不会担心错误配置的ioc容器,因为您的假货或模拟实现不存在于您发送到生产的内容中。烟雾测试会立即捕获。

希望这会有所帮助。

答案 2 :(得分:2)

在书Dependency Injection in .Net中,Mark Seemann将最后一种解决方案称为Bastard Injection,并将其视为反模式。这主要是因为您仍然依赖于具体实现。您应该查看本书以获取更多详细信息。

在你在这里指定的情况下,我似乎很可能会在其他地方有一些其他代码产生任何正在验证的代码。我称之为造物主。反过来,这个造物主很可能也需要一个IChecksumGenerator。在这种情况下,我会让DI容器完全控制依赖。

想象一下,您想要为IChecksumGenerator切换不同的实现。假设我上面说的是真的,你需要用Bastard Injection在2个位置改变它;创作者和验证者。让DI Container控制它意味着它只在一个地方 - 容器配置。

让DI容器控制的另一个好处是,它可以通过引入LSP违规来降低未来更改将MyValidator与具体IChecksumValidator更紧密地耦合的可能性。

答案 3 :(得分:1)

我无法看到MyValidator如何通过算法耦合到IChecksumGenerator。 IChecksumGenerator将向MyValidator提供一份合同,在给定一组输入的情况下,将返回一组输出。

IChecksumGenerator的实现如何计算这些输出与MyValidator 无关。这就是您能够提供测试存根的原因。测试存根在输入和输出之间具有硬编码映射,以便您进行测试。真正的实现将使用算法。

算法可以有许多不同的实现。可以有一个实现优化内存使用,另一个实现速度优化。

只要为每个可能的输入提供正确的输出,MyValidator就不应该关心实现。确保这种情况发生就是测试的全部内容。

但是,如果它真的是通过算法耦合而且无法将两者分开,那么它们可能不应该是单独的类。

答案 4 :(得分:0)

您需要将测试代码与产品代码分开。

您可以使用产品代码:

 var validator = new MyValidator(new MyChecksumGenerator());

在测试代码中:

var validator = new MyValidator(new MyChecksumGeneratorStub());

MyChecksumGeneratorStub实现IChecksumGenerator。