2011-04-26 113 views
3

首先我先介绍一下实现,而不依赖注入(这将打破依赖倒置原则):依赖注入和具体的依赖实现

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和存根checksumGenerator。但是,MyValidator实现在算法上与特定的IChecksumGenerator实现耦合(它不会与任何其他实现一起使用)。因此,一些问题出现:

  1. 介绍
  2. 我们破坏封装为MyValidator(耦合MyChecksumGenerator)的私人实现细节的可能性,不正确IChecksumGenerator将被注入(例如,因为IoC容器配置错误)变外部类

我来到了最好的解决方案是:

public class MyValidator 
{ 
    private readonly IChecksumGenerator _checksumGenerator; 

    public MyValidator() 
    { 
    _checksumGenerator = new MyChecksumGenerator; 
    } 

    internal MyValidator(IChecksumValidator checksumValidator) 
    { 
    _checksumValidator = checksumValidator; 
    } 

    ... 
} 

他为了测试目的,我引入了特殊的构造函数(所以我可以在测试中将IChecksumValidator存根),但是公共构造函数创建了它所耦合的实现(所以封装没有被破坏)。为测试目的创建一些代码有点难看,但在这种情况下看起来有意义。

你会如何解决这个问题?

+1

如果MyValidator仅适用于IChecksumGenerator的一个特定实现,那么如何将其替换为测试双?无论如何,你会违反LSP ... – 2011-04-26 08:12:51

+0

@Mark:关于LSP的好处。请制定它作为答案。但是我没有得到关于测试双打的意见 – SiberianGuy 2011-04-26 09:01:23

+0

@ldsa:添加了一个答案。 – 2011-04-26 09:28:40

回答

4

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

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

在你可以说从某种意义上说,该API在于有关其要求,因为它索赔,它可以处理任何IChecksumGenerator,而在现实中,它只能与一个特定类型的作品 - 让我们把它OneAndOnlyChecksumGenerator。虽然我会建议重新设计坚持LSP的应用程序,你也可以改变构造函数签名是诚实的有关要求:

public class MyValidator 
{ 
    private readonly OneAndOnlyChecksumGenerator checksumGenerator; 

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

    // ... 
} 

您可能仍然能够通过使战略打开OneAndOnlyChecksumGenerator到测试双成员虚拟,以便您可以创建测试特定的子类。

+0

关于LSP你绝对正确 - 我只需要传递更具体的依赖关系 – SiberianGuy 2011-04-27 00:00:36

0

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

产品代码,你可以使用:

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

在测试代码:

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

其中MyChecksumGeneratorStub实现IChecksumGenerator。

3

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

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

希望这会有所帮助。

+0

为什么它不是封装的违规? – SiberianGuy 2011-04-26 08:52:09

+0

它暴露了一个依赖项,而不是依赖项的实现方式。 – 2011-04-26 14:12:41

1

我无法看到MyValidator如何在算法上耦合到IChecksumGenerator。 IChecksumGenerator将向MyValidator提供一个合同,给定一组输入将返回一组输出。

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

算法可以有许多不同的实现。可以有一个优化内存使用情况的实施方案,另一个方案则可以提高速度。

只要为每个可能的输入提供正确的输出,MyValidator就不应该关心实现。确保发生这种情况就是测试的全部内容。但是,如果它确实是算法耦合的,并且没有办法将两者分开,那么它们应该不是单独的类。

2

在书Dependency Injection in .Net中,Mark Seemann称这是最后一个解决方案Bastard Injection,并认为它是一种反模式。这主要是因为你仍然依赖于具体的实现。你应该看看这本书的更多细节。

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

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

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