代码改变世界

再谈ASP.NET Routing中的ParsedRoute

2009-08-24 14:10 Jeffrey Zhao 阅读(...) 评论(...) 编辑 收藏

上午搬家,东西整理得头大,吃了很多灰,有些头晕,不过把东西积累着也不爽,就写了吧。

ParsedRoute是ASP.NET Routing中的内部类,作用是根据既定模式将一段URL解析为一个RouteValueDictionary。上次的文章中我主要谈了如何利用反射使用类库的内部成员,而这次则想分享一些使用ParsedRoute时产生的一些想法。

首先,这里我想谈一下徐风子同学在那篇文章后面所指出的问题。他认为我们不应该使用类库的内部成员,以下是他的原文评论:

用内部函数的风险是,他既然没有推荐,那就有不推荐使用的理由:只适用于特定功能、有特定的局限性……还有版本升级带来的影响等等。

而对于根本不知道内部结构就贸然调用内部方法,再多的单元测试也不能保证正确,人家也根本没有保证过你任何东西,只是你的猜测。

如果真想用类似的功能,有两个途径:

第一,找公共类库,如果是极为通用的功能肯定会有公共类库,一个有承诺的接口支持。

第二,拷贝源码。直接将源码拷贝出来自己的类中使用。这样可以保证代码的稳定性,不会因版本升级改变。而且你也可以分析、改进源码。其实基本上就算是自己的源码了。

我同意他的部分看法,尤其是使用内部函数的风险,这也是“不使用内部函数”最重要的原因。不过我也认为,在某些情况下还是适合使用一个类库的私有实现的。

例如,我们希望开发的功能便是在“扩展现有的实现”,这在一定程度上要求我们的实现与扩展目标较为接近。于是,我们需要的一些基础功能(例如字符串解析),可能也已经是现有实现的一部分,只是由于现有类库没有将其公开,我们往往需要重复开发相同的功能。如果有现成的公开实现,那么自然不会使用这种Hack的方式。但是如果没有,我就会倾向与使用类库内部已有的功能。正是基于这种考虑,我会复用ParsedRoute,因为我希望可以使用ASP.NET Routing中相同的模式,进行相同的字符串捕获和构造功能。

自然,在这之前我会阅读这部分实现的代码,确保它能够满足我的需求,再通过这种方式进行调用。徐同学认为,如果读完之后,应该将其复制出来,放入自己的系统,便于修改。但是有些功能,它对外的接口简单,但是内部实现可能涉及到数千行代码,十几组件之类的交互(例如运用Facade模式的地方),与此相比,我还是倾向于简单地封装一下接口,并使用充足的单元测试来确保这些功能。

在使用内部功能的时候,单元测试尤其重要(当然平时单元测试也是很重要的)。由于内部功能的确是相对容易改变的地方,我们必须使用单元测试来保证正在使用的那部分功能不会出现问题——或者说,一旦出现问题,我们可以立即发现,并将其替换成自己的实现,或改变内部方法的调用方式(毕竟完全大改的可能性也不太高)。如果您没有编写单元测试的习惯,也请务必为这部分功能写单元测试,否则还是放弃这种做法吧。

不过对于一些普遍使用的基础功能(例如一些数据结构,容器等等),我还是倾向与使用公开实现或直接将代码拷贝出来。例如,WPF中提供了internal的ReadOnlyDictionary类型,在需要的时候便会将其实现复制到自己的项目中。甚至于在使用公开的ObservableCollection<T>类时也会这样,因为我不想在一个普通的项目中依赖一个WPF类库——写到这里,我忽然有些理解.NET框架中包含多个哈希表实现的做法了。

真的不容易。

还有便是ParsedRoute的功能,它其实拥有两个接口,Match和Bind:

internal class ParsedRoute
{
    public RouteValueDictionary Match(
        string virtualPath,
        RouteValueDictionary defaultValues)
    {
        ...
    }

    public BoundUrl Bind(
        RouteValueDictionary currentValues,
        RouteValueDictionary values,
        RouteValueDictionary defaultValues,
        RouteValueDictionary constraints)
    {
        ...
    }
}

Match方法的功能是将URL(即virtualPath)解析为一个RouteValueDictionary,而Bind的作用是根据几个RouteValueDictionary集合构造一个URL。但这里我认为,ParsedRoute类的设计是一个典型的反面教材。

例如,在ASP.NET Routing中是这样使用ParsedRoute类的。开发人员使用的是公开的Route类,提供一个模式字符串(如"{controller}/{action}/{id}")和其他一些默认值,约束(constraint)等设置,而Route会把一部分职责交给ParsedRoute完成。在URL解析阶段,Route类会使用Match方法解析字符串,然后在Route类内部进行约束控制。但是在Bind阶段,对于ParsedRoute却在直接使用这些约束。简单地说,ParsedRoute一会儿知道有“约束”这个东西,一会儿又不知道,它的职责在进行不同工作的时候具有比较明显的变化。因此我认为它设计的不合适。

因此,我在使用ParsedRoute的时候,无论是解析还是构造URL时,都不会提供约束条件(今后会有更多相关内容)。

还有便是,contstraints使用RouteValueDictionary进行保存,它其实是一个IDictionary<string, object>容器。在执行的时候,ASP.NET Routing中会判断这个object对象的类型,如果是字符串则把它作为正则表达式来使用,如果是IRouteConstraint对象则另有一番逻辑,否则就会抛出异常。我不喜欢这个设计,这个做法不够面向对象。虽然OO不是需要严格遵循的准则,但是现在的做法隐藏了太多,假设了太多,我并没有看出它有什么好处。难道仅仅是为了方便?

您有什么想法呢?您会如何使用类库内部的功能?您对ParsedRoute的设计怎么看?