控制反转如何帮助我?

时间:2012-03-25 04:29:29

标签: c# dependency-injection inversion-of-control

我正在尝试理解控制反转以及它如何帮助我进行单元测试。我已经阅读了几篇关于IOC的在线解释及其作用,但我只是不太了解它。

我开发了一个示例项目,其中包括使用StructureMap进行单元测试。 StructureMap设置代码如下:

private readonly IAccountRepository _accountRepository

public Logon()
{
    _accountRepository = ObjectFactory.GetInstance<IAccountRepository>();
}

我不理解的是,就像我看到的那样,我可以简单地将上述声明如下:

AccountRepository _accountRepository = new AccountRepository();

它将与先前的代码做同样的事情。所以,我只是想知道是否有人能够以一种简单的方式向我解释,使用IOC的好处是什么(特别是在处理单元测试时)。

由于

3 个答案:

答案 0 :(得分:2)

Inversion of Control是让框架回调用户代码的概念。这是一个非常抽象的概念,它只是描述了库和框架之间的区别,或者它可以被描述为“框架的定义特征”。我们的代码调用了一个库,而框架则控制着我们的代码。任何框架都提供了允许我们插入代码的钩子。

控制反转是一种模式,只有在构建框架时您是框架开发人员时,或者当您是应用程序开发人员与框架代码交互时才能应用。但是,在专门使用应用程序代码时,IoC不适用。

依赖于抽象而不是实现的行为称为依赖性反转,并且应用程序和框架开发人员都可以实现依赖性反转。所以你所说的IoC实际上就是依赖倒置,正如Krzysztof已经评论过:你所做的不是IoC。我将从现在开始讨论依赖倒置。

依赖倒置基本上有两种形式/实现:服务定位器和依赖注入。

使用服务定位器模式,可以从需要依赖关系的类中调用静态工厂。一般来说,它看起来像这样:

public class Service
{
    public void SomeOperation() {
        IDependency dependency = 
            ServiceLocator.GetInstance<IDependency>();
        dependency.Execute();
    }
}

这个示例对您来说应该很熟悉,因为您正在使用Logon方法执行此操作:您正在使用服务定位器模式。

使用依赖注入模式,可以从外部注入类所需的所有依赖项;最好使用构造函数。类本身不负责获取其依赖项。这个责任在调用堆栈中向上移动。使用依赖注入时,前一个类看起来像这样:

public class Service
{
    private readonly IDependency dependency;

    public Service(IDependency dependency)

{             this.dependency =依赖;         }

    public void SomeOperation()

{             this.dependency.Execute();         }     }

两种模式都是依赖性反转,因为在这两种情况下,Service类都不负责创建依赖项,也不知道它正在使用哪种实现。它只是与界面交谈。这两种模式都使您可以灵活地使用类所使用的实现,从而允许您编写更灵活的软件。

然而,服务定位器模式存在许多问题,这就是为什么它被认为是反模式的原因。您已经遇到了这些问题,因为您想知道依赖性反转(在您的情况下服务定位器)如何帮助您进行单元测试。

答案是服务定位器模式对单元测试没有帮助。相反:它使单元测试非常困难。通过让类调用ObjectFactory,您可以在两者之间创建一个硬依赖关系。替换IAccountRepository进行测试也意味着您的单元测试必须使用ObjectFactory。这使您的单元测试更难阅读。但更重要的是,由于ObjectFactory是一个静态实例,所有单元测试都使用同一个实例,这使得很难在每个测试的基础上单独运行测试并交换实现。

我过去使用过服务定位器模式,我处理这个模式的方法是在服务定位器中注册依赖项,我可以逐个线程地更改(使用[ThreadStatic]字段下的盖子)。这允许我并行运行我的测试(默认情况下MSTest会做什么),同时保持测试被隔离。然而,问题在于,这种情况变得非常复杂,各种技术问题使测试变得混乱,这让我花了很多时间来解决这些技术问题,而我本可以写更多的测试。

这些问题的真正解决方案是依赖注入。一旦你通过构造函数注入一个类需要的依赖项,所有这些问题就会消失。这不仅非常清楚了解类所需的依赖项(没有隐藏的依赖项),而且每个单元测试本身都负责注入所需的依赖项。这使得编写测试变得更加容易,并且可以防止您在单元测试中配置DI容器。

进一步阅读:Service Locator is an Anti-Pattern

答案 1 :(得分:1)

这背后的想法是让您可以替换默认的帐户存储库实现,以获得更多单元可测试版本。在单元测试中,您现在可以实例化一个不进行数据库调用的版本,而是返回固定数据。这样,您可以专注于测试方法中的逻辑,并释放对数据库的依赖性。

这在很多层面都比较好: 1)您的测试更稳定,因为您不再需要担心由于数据库中的数据更改而导致测试失败 2)由于您没有调用外部数据源,因此测试运行速度会更快 3)您可以更轻松地模拟所有测试条件,因为您的模拟存储库可以返回测试任何条件所需的任何类型的数据

答案 2 :(得分:0)

回答你问题的关键是可测试性,如果你想管理注入对象的生命周期,或者你想让IoC容器为你做这件事。

比方说,您正在编写一个使用您的存储库的类,并且您想要测试它。

如果您执行以下操作:

public class MyClass
{
   public MyEntity GetEntityBy(long id)
   {
      AccountRepository _accountRepository = new AccountRepository();

      return _accountRepository.GetEntityFromDatabaseBy(id);
   }
}

当您尝试测试此方法时,您会发现存在许多并发症: 1.必须已经设置了数据库。 2.您的数据库需要具有您正在查找的实体的表。 3.您用于测试的ID必须存在,如果因任何原因删除它,那么您的自动化测试现在就会被破坏。

如果您有以下内容:

public interface IAccountRepository
{
   AccountEntity GetAccountFromDatabase(long id);
}

public class AccountRepository : IAccountRepository
{
   public AccountEntity GetAccountFromDatabase(long id)
   {
      //... some DB implementation here
   }
}

public class MyClass
{
   private readonly IAccountRepository _accountRepository;

   public MyClass(IAccountRepository accountRepository)
   {
      _accountRepository = accountRepository;
   }

   public AccountEntity GetAccountEntityBy(long id)
   {
      return _accountRepository.GetAccountFromDatabase(id)
   }
}

现在您可以在不需要数据库的情况下单独测试MyClass类。

这有什么好处?例如,您可以执行类似的操作(假设您使用的是Visual Studio,但同样的原则适用于NUnit):

[TestClass]
public class MyClassTests
{
   [TestMethod]
   public void ShouldCallAccountRepositoryToGetAccount()
   {
      FakeRepository fakeRepository = new FakeRepository();

      MyClass myClass = new MyClass(fakeRepository);

      long anyId = 1234;

      Account account = myClass.GetAccountEntityBy(anyId);

      Assert.IsTrue(fakeRepository.GetAccountFromDatabaseWasCalled);
      Assert.IsNotNull(account);
   }
}

public class FakeRepository : IAccountRepository
{
   public bool GetAccountFromDatabaseWasCalled { get; private set; }

   public Account GetAccountFromDatabase(long id)
   {
      GetAccountFromDatabaseWasCalled = true;

      return new Account();
   }
}

因此,您可以非常自信地测试MyClass类是否使用IAccountRepository实例从数据库获取Account实体,而无需拥有数据库。

你仍然可以在这里做一百万件事来改进这个例子。您可以使用像Rhino Mocks或Moq这样的Mocking框架来创建虚假对象,而不是像我在示例中那样自己编写它们。

通过这样做,MyClass类完全独立于AccountRepository,因此当loosley耦合概念发挥作用并且您的应用程序可测试且更易于维护时。

通过这个例子,您可以看到IoC本身的好处。现在,如果你不使用IoC容器,你必须实例化所有依赖项并在Composition Root中适当地注入它们,或者配置一个IoC容器,以便它可以为你完成。

问候。