65.9K
CodeProject 正在变化。 阅读更多。
Home

使用代理模式注入不可注入的

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.48/5 (8投票s)

2013年10月7日

CPOL

5分钟阅读

viewsIcon

35049

downloadIcon

121

如果您刚接触依赖注入,有时会遇到一个无法注入的依赖项。本文涵盖了这些场景,并概述了如何使用代理模式来解决这个问题。

引言

当我第一次发现依赖注入时,它改变了我编写代码的方式。它使得测试驱动开发成为可能,并且可以在单元测试中模拟依赖项。在早期,我经常因为遇到无法注入的依赖项而感到沮丧,这意味着我无法在单元测试中模拟它。本文将通过三个我经历过的示例,展示如何使用代理模式来解决这些场景。

背景

本文假设您已经熟悉依赖注入及其工作原理。

要了解更多关于代理模式的信息,请参阅这篇文章

值得注意的关于本文的两点:

  • 对代理模式的一些描述提到真实对象与代理对象共享同一个接口。我没有这样做,这一点在下面的场景中会很明显。
  • 许多框架为以下场景提供了内置解决方案,因此您不一定需要使用代理模式(特别是第三个场景,构造函数中的string参数)。

说明问题的场景

以下是我过去遇到过的三个不同场景:

无法模拟静态方法

在我写的一个应用程序中,我需要使用.NET框架中的File对象来保存和打开文件。File是一个static类,包含CreateOpenstatic方法。我需要存根(stub)这些方法,以便依赖于File的对象能够被隔离测试(例如,在不需要实际打开和创建文件的情况下运行)。像Rhino Mocks和MOQ这样的模拟框架无法模拟或存根static方法。

无法模拟扩展方法

过去几年我花了大量时间编写Silverlight和WPF应用程序,并使用出色的PRISM框架来实现MVVM。这个框架的一个强大功能是导航模块。在我的ViewModel测试中,我想断言导航是通过RegionManager.RequestNavigate调用的。虽然RegionManager有一个可以模拟的接口,但RequestNavigate是一个扩展方法。像Rhino Mocks和MOQ这样的模拟框架无法模拟或存根扩展方法。

无法注入构造函数中带有值类型或字符串的对象

最近的一个项目涉及编写一个WPF应用程序,该应用程序使用Google日历服务访问日历。Google的CalendarService有一个单独的构造函数,它接受一个string参数(用于命名应用程序,但在我的例子中,我只是想将其硬编码为null)。一些依赖注入框架不知道如何构造这个类,因为无法知道string应该被设置为哪个值。在这个特定的项目中,我使用的是SimpleInjector。请注意,这是大多数依赖注入框架可以解决的一个示例,但我在这里列出它是为了说明代理模式也可以解决这个问题。

代理模式解决方案

我附加了一个示例项目,其中包含了所有的问题和解决方案。为了专注于技术,我使用了非常简单的示例。

静态方法

以下代码用作static类的示例。

public static class CannotInjectStatic
{
    public static bool GreaterThanFive(int number)
    {
        return number > 5;
    }
} 

第一步是添加一个接口来暴露您想要模拟的static方法。

public interface ICannotInjectStatic
{
    bool GreaterThanFive(int number);
} 

现在,将此接口实现为一个代理类,用于包装真实类。

public class CannotInjectStaticProxy : ICannotInjectStatic
{
    public bool GreaterThanFive(int number)
    {
        return CannotInjectStatic.GreaterThanFive(number);
    }
} 

现在,新的接口和代理类可以注册到依赖注入容器中,并注入到依赖于真实类的对象中。以下示例使用了Unity作为依赖注入容器。

container.RegisterType<ICannotInjectStatic, CannotInjectStaticProxy>(); 

现在可以模拟代理类了,所以使用代理类的您的类可以在单元测试中使用模拟对象来断言调用的正确性。

[Test]
public void DoSomethingInjected_calls_CannotInjectStatic()
{
    // system under test
    _something.DoSomethingInjected();
    // assertions
    _cannotInjectStaticProxy.AssertWasCalled(x => x.GreaterThanFive(Arg<int>.Is.Equal(6)));
} 

模拟对象还可以为单元测试返回虚假响应。

[Test]
public void DoSomethingInjected_calls_CannotInjectStatic([Values(true, false)]bool isGreaterThanFive)
{
    // setup
    _cannotInjectStaticProxy.Expect(
      x => x.GreaterThanFive(Arg<int>.Is.Anything)).Return(isGreaterThanFive);
    // system under test
    var result = _something.DoSomethingInjected();
    // assertions
    Assert.AreEqual(isGreaterThanFive, result);
} 

扩展方法

以下代码用作类和该类的扩展方法的示例。

public class WillBeExtended
{
    public string Name { get { return "Name"; } }
}

public static class CannotInjectExtension
{
    public static string NamePlus(this WillBeExtended willBeExtended)
    {
        return willBeExtended.Name + "Plus";
    }
} 

再次,我们创建一个接口,暴露我们想要模拟的扩展方法。我们还需要在其中包含您希望在代理类中使用的真实类的非扩展方法和属性。

public interface IWillBeExtendedProxy
{
    string NamePlus();
    string Name { get; }
} 

然后,我们在代理类中实现该接口。

public class WillBeExtendedProxy : IWillBeExtendedProxy
{
    private readonly WillBeExtended _willBeExtended;
    public WillBeExtendedProxy()
    {
        _willBeExtended = new WillBeExtended();
    }
    
    public string NamePlus()
    {
        return _willBeExtended.NamePlus();
    }
    public string Name { get { return _willBeExtended.Name; } }
} 

并进行注册。

container.RegisterType<IWillBeExtendedProxy, WillBeExtendedProxy>(); 

并进行模拟。

[Test]
public void DoSomethingInjected_calls_WillBeExtended()
{
    // system under test
    _something.DoSomethingInjected();
    // assertions
    _willBeExtendedProxy.AssertWasCalled(x => x.NamePlus());
} 

构造函数中的字符串参数

注意:如上所述,只有当您的依赖注入框架没有直接解决此问题时,才应考虑此解决方案。大多数框架都可以。

以下代码用作构造函数带有string参数的类的示例。

public class CannotInjectConstructor
{
    private readonly string _name;
    public CannotInjectConstructor(string name)
    {
        _name = name;
    }
    public string Name { get { return _name; } }
} 

为所有您想要模拟的方法和属性创建一个接口。

public interface ICannotInjectConstructor
{
    string Name { get; }
} 

创建代理类,将构造函数参数硬编码为您想要的值。显然,这假定构造函数的值将始终相同,就像上面的真实世界示例那样。

public class CannotInjectConstructorProxy : ICannotInjectConstructor
{
    private readonly CannotInjectConstructor _cannotInjectConstructor;
    public CannotInjectConstructorProxy()
    {
        _cannotInjectConstructor = new CannotInjectConstructor("name");
    }
    public string Name { get { return _cannotInjectConstructor.Name; } }
} 

最后,进行注册。

container.RegisterType<ICannotInjectConstructor, CannotInjectConstructorProxy>(); 

关注点

代理接口应该只暴露您打算使用的方法和属性。例如,File类有50多个方法,但我只需要OpenCreate,所以接口只有这些方法。

代理接口的方法和属性签名应与真实类完全相同。不要试图向这些接口或代理类添加任何逻辑。代理类不能被独立测试,因此其功能应与真实类完全相同。这也不是问题,因为真实类应该由编写代码的第三方完全测试(例如,我们应该假设Microsoft已经彻底测试了File)。

尽量避免为自己的类创建代理类。根据我的经验,这通常表明存在设计问题(例如,您使用了扩展方法来扩展自己的代码——与其创建代理,不如将扩展方法更改为普通方法)。我唯一一次打破这个规则是在增强一个无法更改原始代码的遗留应用程序时。

要查看上述代码的实际运行情况,我已附上一个解决方案,说明了每个场景。我还包含了单元测试,展示了如何模拟和存根依赖项。

历史

  • 2013年10月7日:初始版本
  • 2013年10月8日:添加了模拟示例,并进一步阐明了何时可能使用场景3 
© . All rights reserved.