Hello World
Spiga

再谈ASP.NET Routing中的ParsedRoute

2009-08-24 14:10 by 老赵, 5457 visits

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

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的设计怎么看?

Creative Commons License

本文基于署名 2.5 中国大陆许可协议发布,欢迎转载,演绎或用于商业目的,但是必须保留本文的署名赵劼(包含链接),具体操作方式可参考此处。如您有任何疑问或者授权方面的协商,请给我留言

Add your comment

20 条回复

  1. 我叫沙发[未注册用户]
    *.*.*.*
    链接

    我叫沙发[未注册用户] 2009-08-24 14:11:00

    sf...........

  2. 玉开
    *.*.*.*
    链接

    玉开 2009-08-24 15:00:00

    老赵的板凳要坐一下

  3. 老翁
    *.*.*.*
    链接

    老翁 2009-08-24 15:09:00

    搬家了也不忘写东西,老赵的敬业精神和对技术的这种值得学习。

  4. 老翁
    *.*.*.*
    链接

    老翁 2009-08-24 15:15:00

    实话说,类库内的功能用的不多,如果有些相对比较独立的函数或功能比较好且与之相关的组件不太多的话,直接复制出来比较妥当吧。

  5. 麒麟.NET
    *.*.*.*
    链接

    麒麟.NET 2009-08-24 16:14:00

    老翁:实话说,类库内的功能用的不多,如果有些相对比较独立的函数或功能比较好且与之相关的组件不太多的话,直接复制出来比较妥当吧。


    有些内部类是无法简单地复制出来的,因为这些内部类很可能引用了其他内部类

  6. Ivony...
    *.*.*.*
    链接

    Ivony... 2009-08-24 16:15:00

    内部类会造成版本依赖,这是无法回避的风险,应该至少加个Assembly的版本检查。

    其他的,原则就是用来违背的,嗯。。。。

  7. 老赵
    admin
    链接

    老赵 2009-08-24 16:18:00

    @Ivony...
    之前那篇文章中的代码typeof(Route).GetType本身就默认有了版本检查,确切地说是CLR帮忙做的。
    // 我一直没理解“原则就是用来违背”是啥意思,具体说说?

  8. Ivony...
    *.*.*.*
    链接

    Ivony... 2009-08-24 19:15:00

    刚去看了一下代码,typeof(Route).Assembly.GetType对于编译后的程序,是与具体的版本挂钩绑定的。因为这个Route是版本限定的,从而限定了ParsedRoute。但是如果代码放到新的ASP.NET MVC版本下编译,又很不幸的是,这个新版本的ASP.NET MVC正如上面所说的改了internal的实现,就会出现不易察觉的问题。

    所以:
    var routeType = typeof(Route).Assembly.GetType("System.Web.Routing.ParsedRoute");
    在这段代码后,加上:
    if ( routeType.Assembly.GetName().Version.ToString() != "xxx.xxx.xxx.xxx" )
    {
    throw new TypeLoadException( "版本不匹配,此方法需要xxx.xxx.xxx.xxx版本的ASP.NET MVC" );
    }
    则更稳妥。

    发现博客园的code标签还是有问题,还是改回来算了。

  9. 老赵
    admin
    链接

    老赵 2009-08-24 20:20:00

    @Ivony...
    你说的属于快速地透明地汇报错误,但是我觉得,假如需要重新编译的话,那么我就会重跑单元测试,这个似乎更稳妥……

  10. Ivony...
    *.*.*.*
    链接

    Ivony... 2009-08-24 20:39:00

    谈谈原则的问题吧,其实任何东西的发展史恐怕都是一个对原有原则违背的过程。软件更是如此,就在.NET这短短几年的发展史中,对原有规则、约定、习惯甚至原则的违背也不在少数。比如说.NET在一开始就开始就对匈牙利命名法原则进行否定,如果我没记错的话,匈牙利命名法的设计者后来就是微软的某个大牛。而MFC对这条规则的贯彻始终,到.NET的明确摒弃,时间并不长。。。。

    又比如C# 4.0中增加的可选参数和命名参数,命名参数按下不表,没记错的话,C#拒绝可选参数可是Anders在设计C#的时候明确声明的一个原则。在早期的VS帮助或者现在的VS帮助中都应该能找到相关的介绍。。。。

    LINQ to XML中的方法命名,Elements而不是GetElements岂不又是对以前的命名规则的违背?XNamespace的+运算符的重载,在几年前恐怕也会被认为是一个不合理的重载吧。

    而.NET恐怕也正是因为对旧有东西的摒弃才得到如此飞快的发展的吧。所以当你确实认为你明确的了解了那些原则形成或制定的背景,并确保违背他们并不会出现他们所担心的问题,或者那些东西早已不合时宜。那就从那些破规则上践踏过去吧,固步自封的Java时代已经过去了。

  11. 老赵
    admin
    链接

    老赵 2009-08-24 20:43:00

    @Ivony...
    你说的大道理是没错,我很同意,但是我想讨论的其实是这里的小问题……赫赫。

  12. Ivony...
    *.*.*.*
    链接

    Ivony... 2009-08-24 21:02:00

    Jeffrey Zhao:
    @Ivony...
    你说的大道理是没错,但是我想讨论的其实是这里的小问题……



    呵呵,你说起了这个,我就顺便说说。

    其实我的观点很简单哈,既然已经很明确的知道这样将造成版本依赖,并能够看到里面潜在的风险,又是用在完全可信的ASP.NET环境。这样用并没有什么问题哈。微软internal这个东西自然有它的道理,可能更多的考虑是以后还要增加功能吧。利用反射来调用一个非public成员还有一个潜在的风险就是可能会受到CAS的拦截,不知道有人指出来没。当然,调用非公开成员还会给安全方面带来其他薄弱环节,但在此例中,反正ASP.NET绝大时候运行在完全受信环境,则无需有这方面的考虑。

  13. 老赵
    admin
    链接

    老赵 2009-08-24 21:06:00

    @Ivony...
    我很欣赏你的想法啊,赫赫。
    刚才洗澡时忽然在想这种私有方法调用,和单元测试之间是否可以有所融合。
    例如private方法的测试,我觉得可以尝试一下。
    // 主要是指上一篇文章中我设想的类库。

  14. Ivony...
    *.*.*.*
    链接

    Ivony... 2009-08-24 21:17:00

    其实我这个人比较传统,Reflector的代码复制出来我觉得是更不可取的事情,照着自己重写一份,public出来倒是个不错的选择。我以前干过这事儿,所以我只是认为老赵的做法并没有什么值得特别大惊小怪,真正自己遇到这种情况,可能还是自己写。

    如果真的是想偷懒提取实现,用IL反编译器反编译,挖出来再直接编译IL做成Module并到Assembly可能更保险。老实说我不是很相信Reflector出来的代码。希望以后能够有从Assembly里面提取类型之类的工具。

    当然说破了天,测试永远是必要的。这样做的最大的风险我认为还不在与版本依赖和代码安全这两个显而易见的方面。最大的风险是看不到的风险,即脑子中存在的“微软的代码都是没问题的,微软的方法都是没问题的,直接拿来用肯定是没问题的”,这样的思维可能才是最大的风险。

  15. 老赵
    admin
    链接

    老赵 2009-08-24 21:21:00

    @Ivony...
    为什么不相信Reflector出来的代码?
    从Assembly里面提取类型之类的工具是指什么?

  16. Ivony...
    *.*.*.*
    链接

    Ivony... 2009-08-24 21:29:00

    1、首先Reflector反编译出来的不是源代码,所以这些代码再编译回去也基本不会得到相同的IL(有的甚至没法编译)。当然还有可读性的问题。

    2、就是可以从Assembly里面提取一个类型的所有元数据和IL的工具啊,目前好像还没有。

  17. 老赵
    admin
    链接

    老赵 2009-08-24 23:08:00

    @Ivony...
    我倒不介意编译回去的IL和原来不同,本来就是要相同功能,不需要IL级别完全一致。没法编译我倒没有遇见过,能不能举一些例子?
    至于第二个,其实基础类库已经有了,估计封装一下会是你要的功能:
    http://cciast.codeplex.com/
    http://ccimetadata.codeplex.com/

  18. 韦恩卑鄙
    *.*.*.*
    链接

    韦恩卑鄙 2009-08-25 10:48:00

    老赵 vb的re成c#基本没有能编译的 oh yeah

    with end with 是个祸害阿

    一切vb临时量(年八大阿 with阿) 都给加了$

  19. 韦恩卑鄙
    *.*.*.*
    链接

    韦恩卑鄙 2009-08-25 10:49:00

    所以说要是看dnn上面有个实现很好 第一反应肯定不是reflector 二是看公布源代码

  20. vczh[未注册用户]
    *.*.*.*
    链接

    vczh[未注册用户] 2009-08-25 17:02:00

    这种逻辑到处都有,譬如说ComboBox.DataSource文档说要求一个IList,那为什么接口是Object呢?

发表回复

登录 / 登录并记住我 ,登陆后便可删除或修改已发表的评论 (请注意保留评论内容)

昵称:(必填)

邮箱:(必填,仅用于Gavatar

主页:(可选)

评论内容(大于5个字符):

  1. Your Name yyyy-MM-dd HH:mm:ss

使用Live Messenger联系我