依赖可注射性是依赖注入的唯一理由吗?

时间:2010-06-06 20:17:59

标签: c# unit-testing design-patterns dependency-injection dependencies

据我所知,DI的优点是:

  • 减少依赖性
  • 更多可重用代码
  • 更多可测试代码
  • 更易读的代码

假设我有一个存储库OrderRepository,它充当通过Linq to Sql dbml生成的Order对象的存储库。我无法使我的订单存储库通用,因为它执行Linq Order实体和我自己的Order POCO域类之间的映射。

由于OrderRepository必然依赖于特定的Linq to Sql DataContext,因此无法真正说明DataContext的参数传递使代码可重用或以任何有意义的方式减少依赖。

它还使代码更难阅读,以实例化我现在需要编写的存储库

new OrdersRepository(new MyLinqDataContext())

另外与存储库的主要目的相反,即抽象/隐藏DataContext的存在以消耗代码。

总的来说,我认为这将是一个非常可怕的设计,但它会带来便利单元测试的好处。这有充分的理由吗?还是有第三种方式?我对听取意见非常感兴趣。

10 个答案:

答案 0 :(得分:12)

依赖注入的主要优势在于测试。当我第一次开始采用测试驱动开发和DI时,你已经找到了一些对我来说很奇怪的东西。 DI确实破坏了封装。单元测试应测试与实现相关的决策;因此,您最终会暴露出纯粹封装的场景中不会出现的细节。您的示例很好,如果您没有进行测试驱动开发,您可能希望封装数据上下文。

但是你说,因为OrderRepository必然依赖于特定的Linq to Sql DataContext ,我不同意 - 我们有相同的设置,只依赖于接口。你必须打破这种依赖。

更进一步的例子,如何在不运行数据库的情况下测试存储库(或客户端)?这是单元测试的核心原则之一 - 您必须能够在没有与外部系统交互的情况下测试功能。没有比数据库更重要的了。依赖注入是一种模式,可以破坏对子系统和层的依赖。没有它,单元测试最终需要大量的夹具设置,变得难以编写,易碎且太慢。结果 - 你只是不会写它们。

让你的例子更进一步,你可能有

单元测试:

// From your example...

new OrdersRepository(new InMemoryDataContext());

// or...

IOrdersRepository repo = new InMemoryDataContext().OrdersRepository;

和生产中(使用IOC容器):

// usually...

Container.Create<IDataContext>().OrdersRepository

// but can be...

Container.Create<IOrdersRepository>();

(如果你没有使用过IOC容器,它们就是使DI工作的粘合剂。把它想象成对象图的“make”(或蚂蚁)......容器为你构建依赖图和做所有繁重的施工)。在使用IOC容器时,您将获得在OP中提到的依赖项隐藏。依赖关系由容器配置和处理,作为一个单独的问题 - 调用代码可以只询问接口的实例。

这本书非常出色,详细探讨了这些问题。查看Mezaros的 xUnit测试模式:重构测试代码。这是将您的软件开发能力提升到新水平的那本书之一。

答案 1 :(得分:3)

依赖注入只是意味着结束。这是enable loose coupling的一种方式。

答案 2 :(得分:2)

小评论:依赖注入= IoC +依赖性反转。对于测试和实际描述的内容最重要的是依赖反转

一般来说,我认为测试证明依赖倒置是正确的。但它不能证明依赖注入的合理性,我不会仅仅为测试引入DI容器。

然而,依赖倒置是一个可以在必要时稍微弯曲的原则(像所有原则一样)。您可以特别在某些地方使用工厂来控制对象的创建。

如果您有DI容器,那就是自动发生的事情; DI容器充当工厂并将物体连接在一起。

答案 3 :(得分:2)

依赖注入的优点是能够隔离组件

隔离的一个副作用是更容易的单元测试。另一个是能够交换不同环境的配置。另一个是每个阶级的自我描述性质。

然而,您利用DI提供的隔离功能,您可以获得比使用更紧密耦合的对象模型更多的选项。

答案 4 :(得分:1)

使用Inversion of Control容器(如StructureMap)时会产生依赖注入的强大功能。使用它时,您不会在任何地方看到“新” - 容器将控制您的对象构造。这样一切都不知道依赖。

答案 5 :(得分:1)

我发现需要依赖注入容器和可测试性之间的关系是另一种方式。我发现DI非常有用因为我编写单元可测试代码。我编写单元可测试代码,因为它本身就是更好的代码 - 更松散地耦合更小的类。

IoC容器是工具箱中的另一个工具,可以帮助您管理应用程序的复杂性 - 并且它运行良好。我发现它允许通过从图片中进行实例化来更好地编码到接口。

答案 6 :(得分:1)

这里的测试设计总是取决于SUT(被测系统)。问自己一个问题 - 我想测试什么?

如果您的存储库只是数据库访问者 - 则需要像访问者一样对其进行测试 - 数据库参与。 (实际上这种测试不是单元测试而是集成)

如果你的存储库执行一些映射或业务逻辑并且像数据库的访问者一样,那么当需要进行分解以使你的系统符合SRP时就是这种情况(单一责任原则)。分解后,您将拥有2个实体:

  1. OrdersRepository
  2. OrderDataAccessor
  3. 彼此分开测试,打破DI的依赖。

    构造函数丑陋...使用DI框架构造对象。例如,使用Unity构造函数:

    var repository = new OrdersRepository(new MyLinqDataContext());
    

    将如下所示:

    var repository = container.Resolve<OrdersRepository>;
    

答案 7 :(得分:1)

关于在你的问题中使用DI,陪审团仍然没有。你曾经问过,单独测试是否是实施DI的理由,而且在回答这个问题时我会听起来有点像围栏保姆,即使我的直觉反应是回答否。

如果我的回答是肯定的,那么当你没有任何东西可以直接测试时,我正在考虑测试系统。在物理世界中,包括端口,访问隧道,接线片等并不罕见,以便提供一种简单直接的方法来测试系统,机器等的状态。这在大多数情况下似乎是合理的例如,输油管道提供检查舱口,以允许将设备注入系统以进行测试和维修。这些是专门构建的,不提供其他功能。但真正的问题是,这种范式是否适合软件开发。我的一部分想说是的,但看起来答案会付出代价,让我们处于平衡利益与成本之间的可爱灰色区域。

“不”的论点实际上归结为设计软件系统的原因和目的。 DI是一种促进代码松散耦合的漂亮模式,我们在OOP类中教授的是一个非常重要且功能强大的设计概念,用于提高代码的可维护性。问题是,像所有工具一样,它可能被滥用。我将部分地不同意Rob的回答,因为DI的优势主要不是测试,而是促进松耦合架构。我认为,仅依靠测试它们的能力来设计系统就可以证明,在这种情况下,架构存在缺陷,或者测试用例配置不当,甚至两者都可能。

在大多数情况下,一个考虑周全的系统架构本身就很容易测试,过去十年中引入的模拟框架使得测试更加容易。经验告诉我,我发现难以测试的任何系统都有某些方面,它在某种程度上过于紧密耦合。有时(更少见)这被证明是必要的,但在大多数情况下它并非如此,并且通常当一个看似简单的系统似乎难以测试时,这是因为测试范例存在缺陷。我已经看到DI被用作绕过系统设计的手段,以便允许对系统进行测试,并且风险肯定超过了预期的奖励,系统架构有效地被破坏了。我的意思是后门代码导致安全问题,代码膨胀与测试特定的行为,从未在运行时使用,以及源代码的spaghettification,你需要几个夏尔巴和一个Ouija董事会只是为了弄清楚方式了!所有这些都在发货的生产代码中。在维护,学习曲线等方面产生的成本可能是天文数字,对于小公司来说,这种损失在长期内可能是毁灭性的。

恕我直言,我不相信DI应该被用作简单地提高代码可测试性的手段。如果DI是您测试的唯一选择,那么通常需要对设计进行重构。另一方面,通过设计实现DI,它可以作为运行时代码的一部分,可以提供明显的优势,但不应该被滥用,因为它也可能导致类被滥用,并且它不应该超过 - 之所以使用它只是因为它看起来很酷而且容易,因为在这种情况下它可能会使代码的设计过于复杂。

: - )

答案 8 :(得分:1)

目前,依赖注入被广泛使用。本文解释了依赖注入的所有优点。 http://venkataspinterview.blogspot.com/2011/06/interview-questions-related-to.html

答案 9 :(得分:0)

可以用Java编写通用数据访问对象:

package persistence;

import java.io.Serializable;
import java.util.List;

public interface GenericDao<T, K extends Serializable>
{
    T find(K id);
    List<T> find();
    List<T> find(T example);
    List<T> find(String queryName, String [] paramNames, Object [] bindValues);

    K save(T instance);
    void update(T instance);
    void delete(T instance);
}

我不能代表LINQ,但.NET有泛型,所以它应该是可能的。