代码改变世界

为什么是HttpContextBase而不是IHttpContext

2009-08-21 15:15  Jeffrey Zhao  阅读(...)  评论(... 编辑 收藏

我们知道,由于HttpContext很难进行Mock,因此为了提高可测试性,微软随ASP.NET MVC发布了一个“抽象包”,专门用于对HttpContext及其相关组件进行抽象。不过在Preview 1版本中,这些抽象都是一个个接口,如IHttpContext,IHttpRequest等等。而在下一个版本中,立即就成为了一个个抽象类,如HttpContextBase,HttpRequestBase。从命名上来说,我喜欢接口的命名方式,清晰而好用。而在自己的项目中,我也倾向于使用接口而不是抽象类。但是对于ASP.NET Abstractions中的这些抽象类型,我倒觉得“的确应该如此”。除了在公开API中应该谨慎地使用接口这一原因(而对于HttpContext这种拥有数十个成员的类型更不应该使用接口),有些朋友也提到HttpContext是一个实际的概念,而接口表示的是“can do”,因此应该使用抽象类。不过,现在我打算从“使用”角度来谈一下,为什么这里的确应该用抽象类而不是接口。

关于这点,我们应该直接来看HttpContextBase这个抽象类的写法:

public abstract class HttpContextBase : IServiceProvider
{
    protected HttpContextBase() { }

    public virtual void AddError(Exception errorInfo)
    {
        throw new NotImplementedException();
    }

    public virtual void ClearError()
    {
        throw new NotImplementedException();
    }

    ...
}

我们可以看到,虽然HttpContextBase是抽象类,但是其中没有任何一个成员是抽象的,只不过每个成员都会直接抛出NotImplementedException。为什么这样?

这是由这个类的作用决定的。ASP.NET Abstractions中的类都是为了提高“可测试性”,而是用手段就是提供一个“抽象”以便创建Mock对象。一个Mock对象的作用,简单地说便是模拟真实对象的行为,然后交给被测试功能使用,以此判断被测试功能是否正确。例如,我们使用Moq框架可以轻松地创建HttpContext的Mock对象:

var mockContext = new Mock<HttpContextBase>();
mockContext.Setup(c => c.Request.Form).Returns(new NameValueCollection());
DoSomething(mockContext.Object);

由于DoSomething基于HttpContext的抽象HttpContextBase编写,因此我们可以准备一个Mock对象,并使它的Request.Form属性返回我们需要的内容,这样DoSomething方法内部就可以访问到我们所期望的值了。但实际上,Moq等Mock框架的工作方式,都是动态生成一些类型,继承抽象类(或实现接口),并根据我们的“设置”来实现一些成员。因此,它们基本上都要对被Mock的对象有一定要求,例如不允许是一个密闭(sealed)类,而被操作的成员也必须是abstract或virtual的,因为只有这样,动态类型才能替换掉这些成员的实现1

显然,HttpContextBase满足这个要求。事实上,Mock框架只是帮我们省去了编写下面这些代码的负担(当然写法可以不同):

public class MockHttpContext : HttpContextBase
{
    public override HttpRequestBase Request
    {
        get
        {
            return this.MockRequest;
        }
    }

    public HttpRequestBase MockRequest { get; set; }
}

public class MockHttpRequest : HttpRequestBase
{
    public override NameValueCollection Form
    {
        get
        {
            return this.MockForm;
        }
    }

    public NameValueCollection MockForm { get; set; }
}

试想,如果HttpContextBase是IHttpContext接口怎么办?没错,除了我们需要的成员之外,我们还必须准备一大堆我们不需要的成员的“空架子”——同样道理,如果拥有抽象的成员也会遇到这个情况。因此,虽然HttpContextBase是抽象类,但是没有任何一个抽象成员。这意味着当我们需要手动实现Mock对象的时候,只需要覆盖我们的目标成员就可以了。

Pragmatic Programmer》告诉我们:“不要基于假设编程”,因此在设计HttpContext抽象的时候,也不应该假设“Mock框架在任何情况下都是最好的选择”。因此,我认为HttpContextBase是非常合适的。

 

注1:使用CLR Profiler类似机制的Mock框架TypeMock不会收到这个约束,它可以任意操作密闭类或方法——只可惜它是收费框架。不过,如果Moq或Rhino Mocks等“普通”Mock框架无法满足您需求的话,这基本上可以算是您的设计问题:缺少可测试性。