Hello World
Spiga

复用类库内部已有功能

2009-08-19 18:59 by 老赵, 7397 visits

经常看我博客的人可能会知道,我是一个喜欢搞点小技巧来实现某个功能的人。例如博客的皮肤,自己花了不少时间定义,也是为了效果丰富一些。当然,搞得最多的是从框架或类库内部取出一点小功能来用用,节省自己开发的时间。这其实也是一种复用,尤其是开发一些“扩展”的时候,例如当时尝试为UpdatePanel增加上传功能,虽然最后的结果不是很理想,但是大部分的Hack以及前后端的交互是非常成功的(最大的问题在于跨浏览器实现iframe通信)。而现在也打算总结一次这方面的简单技巧,为以后的文章贡献点引用资源。

这次我们想“复用”的内容是ASP.NET URL Routing中“解析URL”的功能。具体一点地说,就是把一个字符串根据指定的Pattern拆分成键/值对的功能。从.NET Reflector反编译System.Web.Routing.dll的结果来看,这部分的解析工作是交由RouteParser和ParsedRoute两个类完成的。这里引用一下相关的使用代码,如果您感兴趣的话,也可以阅读它们完整的实现:

public class Route
{
    public string Url
    {
        get { ... }
        set
        {
            this._parsedRoute = RouteParser.Parse(value);
            this._url = value;
        }
    }

    public override RouteData GetRouteData(HttpContextBase httpContext)
    {
        string virtualPath = ...
        RouteValueDictionary values = this._parsedRoute.Match(virtualPath, this.Defaults);

        ...
    }

    ...
}

从代码中可以看出,RouteParser的作用是将一个Pattern(如"{controller}/{action}/{id}")转化成一个“解析器”,而这个解析器便是ParsedRoute类。在需要拆分一个URL字符串(如"Home/Index/5")的时候,便会调用ParsedRoute类的Match方法,由此得到一个RouteValueDictionary对象,其中包含了Pattern中定义的名称,和一些值的映射关系。

可能您也能够轻易实现这样的功能,不过既然微软已经帮我们做好了,我们也不妨直接使用一下,偶尔用来拆拆字符串也是挺方便的。只可惜RouteParser和ParsedRoute都是由internal修饰的,我们无法直接访问到。那么就用点小技巧吧……说实话,其实您会发现也就这么一回事,“反射”罢了。因此,我们便学着ASP.NET Routing的做法,构建两个类吧:

internal static class RouteParser
{
    public static ParsedRoute Parse(string routeUrl) { ... }
}

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

我们目前的做法算是一种Hack,为了保证其可维护性,我会选择与目标类库/框架的接口尽可能完全一致的做法。这么做的好处在于,我可以很轻易地理解正在实现的功能,一旦出现了任何问题,就可以直接去找对应的内部实现,而不用在一堆堆的反射关系中“翱翔”。

接着便可以实现我们需要的效果了。在这里,我使用了FastReflectionLib来加快反射调用的性能。虽然我不是一个追求性能极致的Geek,但是如果有一种几乎不耗费额外代价,就能得到数百倍的性能提升,何乐而不为呢?

internal static class RouteParser
{
    private static MethodInvoker s_parseInvoker;

    static RouteParser()
    {
        var parserType = typeof(Route).Assembly.GetType("System.Web.Routing.RouteParser");
        var parseMethod = parserType.GetMethod("Parse", BindingFlags.Static | BindingFlags.Public);
        s_parseInvoker = new MethodInvoker(parseMethod);
    }

    public static ParsedRoute Parse(string routeUrl)
    { 
        return new ParsedRoute(s_parseInvoker.Invoke(null, routeUrl));
    }
}

internal class ParsedRoute
{
    private static MethodInvoker s_matchInvoker;

    static ParsedRoute()
    {
        var routeType = typeof(Route).Assembly.GetType("System.Web.Routing.ParsedRoute");
        var matchMethod = routeType.GetMethod("Match", BindingFlags.Instance | BindingFlags.Public);
        s_matchInvoker = new MethodInvoker(matchMethod);
    }

    private object m_instance;

    public ParsedRoute(object instance)
    {
        this.m_instance = instance;
    }

    public RouteValueDictionary Match(string virtualPath, RouteValueDictionary defaultValues)
    {
        return (RouteValueDictionary)s_matchInvoker.Invoke(this.m_instance, virtualPath, defaultValues);
    }
}

两个类其实都是使用反射,从类库中获取合适的MethodInfo,然后交给MethodInvoker去执行。其他的……由于代码过于简单,我都不知道还需要解释什么东西。最后就使用xUnit测试一下吧:

public class ParseRouteTest
{
    [Fact]
    public void Basic_Parsing()
    {
        var parsedRoute = RouteParser.Parse("{controller}/{action}/{id}");
        var values = parsedRoute.Match("Home/Index/5", null);
        Assert.Equal("Home", values["controller"]);
        Assert.Equal("Index", values["action"]);
        Assert.Equal("5", values["id"]);
    }
}

说实话,这个方法并没有太多技术含量,由于我们将自己的实现和目标实现完全对应起来,所以我们所要做的,似乎也都是些机械的“映射”功能而已。这就引发了我的一个想法,既然很“机械”,那么为什么不去让它“自动”完成呢?例如,我们完全可以写一个类库,来实现这样的效果:

[Type("System.Web.Routing.ParsedRoute, ...")]
interface IParsedRoute
{
    RouteValueDictionary Match(string virtualPath, RouteValueDictionary defaultValues);
}

[Type("System.Web.Routing.RouteParser, ...")]
interface IRouteParser
{
    [Static]
    IParsedRoute Parse(string url);
}

通过定义接口和标记,我们可以直接“声明”需要“挖掘”出来的类型是什么。然后自然可以有框架为我们进行匹配:

IRouteParser parser = HackFactory.Create<IRouteParser>();
IParsedRoute route = parser.Parse("{controller}/{action}/{id}");
RouteValueDictionary values = route.Match("Home/Index/5", null);

是不是一下子变得爽快了许多?简单想了想,这样的框架从技术上来说似乎并没有太多困难。

Creative Commons License

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

Add your comment

52 条回复

  1. 温景良(Jason)
    *.*.*.*
    链接

    温景良(Jason) 2009-08-19 19:29:00

    没看明白啥意思

  2. 冬之心
    *.*.*.*
    链接

    冬之心 2009-08-19 19:29:00

    沙发 学习了~

  3. DiggingDeeply
    *.*.*.*
    链接

    DiggingDeeply 2009-08-19 19:34:00

    老赵,你这博客皮肤在linux的FF下死活打不开。
    我只好切到Windows下,

    对了,你最近蛮高产,快赶上那个吉日旮旯了。

    待会再细看,机器updating中。

  4. 温景良(Jason)
    *.*.*.*
    链接

    温景良(Jason) 2009-08-19 19:43:00

    谁能告诉我这个hack到底是指什么意思啊,查了金山词霸,看得很怪异

  5. 李贵庆
    *.*.*.*
    链接

    李贵庆 2009-08-19 19:52:00

    挺有意思,特殊环境下很有用。

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

    韦恩卑鄙 2009-08-19 20:00:00

    这人过于淫荡了

  7. ~~[未注册用户]
    *.*.*.*
    链接

    ~~[未注册用户] 2009-08-19 20:17:00

    你为什么这么痛恨IE??

  8. fuck sb[未注册用户]
    *.*.*.*
    链接

    fuck sb[未注册用户] 2009-08-19 20:19:00

    此人该吃药了!

  9. 老赵
    admin
    链接

    老赵 2009-08-19 20:30:00

    温景良(Jason):没看明白啥意思


    哪里没有看明白?

  10. 老赵
    admin
    链接

    老赵 2009-08-19 20:31:00

    DiggingDeeply:
    老赵,你这博客皮肤在linux的FF下死活打不开。
    我只好切到Windows下。


    不会啊,我平时也用用Linux的。

  11. 老赵
    admin
    链接

    老赵 2009-08-19 20:31:00

    ~~:你为什么这么痛恨IE??


    我不是痛恨IE,我是痛恨IE6,谢谢。

  12. 老赵
    admin
    链接

    老赵 2009-08-19 20:31:00

    温景良(Jason):谁能告诉我这个hack到底是指什么意思啊,查了金山词霸,看得很怪异


    就是用投机取巧的办法解决问题的意思。

  13. 老赵
    admin
    链接

    老赵 2009-08-19 20:32:00

    韦恩卑鄙:这人过于淫荡了


    点解?

  14. DiggingDeeply
    *.*.*.*
    链接

    DiggingDeeply 2009-08-19 21:12:00

    看了,奇技淫巧。

  15. Grove.Chu
    *.*.*.*
    链接

    Grove.Chu 2009-08-19 21:28:00

    老赵真是精力充沛
    一天两篇……
    不敢想象

  16. 鹤冲天
    *.*.*.*
    链接

    鹤冲天 2009-08-19 21:30:00

    非常佩服老赵最后的想法!

  17. 老赵
    admin
    链接

    老赵 2009-08-19 22:12:00

    @鹤冲天
    我现在又在为取名发愁了,各位有没有什么好建议?

  18. 老赵
    admin
    链接

    老赵 2009-08-19 22:12:00

    DiggingDeeply:看了,奇技淫巧。


    嗯嗯,最后个框架比较有价值。

  19. DiryBoy
    *.*.*.*
    链接

    DiryBoy 2009-08-19 22:17:00

    感觉像 XyzAccessor 的味道~~

  20. 老赵
    admin
    链接

    老赵 2009-08-19 22:43:00

    DiryBoy:感觉像 XyzAccessor 的味道~~


    这是什么?搜不到信息啊。

  21. dsdf[未注册用户]
    *.*.*.*
    链接

    dsdf[未注册用户] 2009-08-19 22:52:00

    老赵的英语 routing [ru:ting] not [rauting]

  22. 老赵
    admin
    链接

    老赵 2009-08-19 23:03:00

    @dsdf
    多谢提醒啊,太感谢了。

  23. dsdf[未注册用户]
    *.*.*.*
    链接

    dsdf[未注册用户] 2009-08-19 23:11:00

    呵呵,我也经常犯类似错误,说得老外变成真老外来,呵呵

  24. 斯克迪亚
    *.*.*.*
    链接

    斯克迪亚 2009-08-20 01:55:00

    老赵太厚道了,自己都不推荐自己的文章,我帮你推上去1点哈。

  25. Arthraim
    *.*.*.*
    链接

    Arthraim 2009-08-20 07:21:00

    看明白了作案动机,没太看明白作案手法……
    话说微软不是“帮我们”写了,只是我们看到了而已,哈哈

  26. 老赵
    admin
    链接

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

    @斯克迪亚
    谢谢,不过推荐这东西,现在看起来没有啥意义啊。

  27. 老赵
    admin
    链接

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

    @Arthraim
    作案手法只是反射咯。

  28. 鹤冲天
    *.*.*.*
    链接

    鹤冲天 2009-08-20 08:58:00

    @Jeffrey Zhao
    名字确实不好起啊!需要些“灵感”,先多用上几次说不定就有了!

  29. CPU风扇
    *.*.*.*
    链接

    CPU风扇 2009-08-20 09:04:00

    文章排版让人看了很舒服

  30. Haozes
    *.*.*.*
    链接

    Haozes 2009-08-20 10:06:00

    文章不能RSS全部浏览,郁闷.开全文吧.哥.

  31. skyaspnet
    *.*.*.*
    链接

    skyaspnet 2009-08-20 10:09:00

    您好,现在的这个模板是否加上“收藏”的链接比较好?只有个“网摘”链接,感觉不是太方便,谢谢!

  32. 老赵
    admin
    链接

    老赵 2009-08-20 10:16:00

    @skyaspnet
    这个和dudu提一下吧,我做不到的……

  33. 老赵
    admin
    链接

    老赵 2009-08-20 10:17:00

    @Haozes
    这需要手动为没篇文章开,我总是忘,后来就全忘了……

  34. Haozes
    *.*.*.*
    链接

    Haozes 2009-08-20 10:21:00

    @Jeffrey Zhao
    不过来看看评论也好.RSS MS没有这个定义

  35. superstar
    *.*.*.*
    链接

    superstar 2009-08-20 11:34:00

    不容易看懂呀

  36. 徐风子
    *.*.*.*
    链接

    徐风子 2009-08-20 13:51:00

    反射调用库的私有方法??有点奇怪,这么用也没什么意思。

  37. 老赵
    admin
    链接

    老赵 2009-08-20 13:53:00

    @徐风子
    这个“意思”是指“有趣”还是“意义”呢?

  38. 徐风子
    *.*.*.*
    链接

    徐风子 2009-08-20 14:00:00

    都有,一般公共库中没有文档说明的东西都是不推荐用的。更不用说私有函数了。
    解析URL又不是很难的事情…………

  39. 老赵
    admin
    链接

    老赵 2009-08-20 14:10:00

    @徐风子
    是,的确不推荐用,不过我还是倾向于使用现有的东西。
    解析URL是不难,但是还是要花时间的。你可以试试看实现一个ParsedRoute和RouteParser,也有1000多行代码啊。
    我现在1小时就搞好了,还包括单元测试。
    // 还有,这只是个演示,平时我们可以使用内部更复杂的东西,呵呵。

  40. 徐风子
    *.*.*.*
    链接

    徐风子 2009-08-20 15:11:00

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

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

    如果真想用类似的功能,有两个途径:
    第一,找公共类库,如果是极为通用的功能肯定会有公共类库,一个有承诺的接口支持。
    第二,拷贝源码。直接将源码拷贝出来自己的类中使用。这样可以保证代码的稳定性,不会因版本升级改变。而且你也可以分析、改进源码。其实基本上就算是自己的源码了。

  41. 老赵
    admin
    链接

    老赵 2009-08-20 15:17:00

    @徐风子
    能不能举例说明,为什么单元测试不能保证正确性?
    我也不需要在任何场合使用内部功能,而单元测试已经可以确保“在我需要的场合”,那些内部功能可以使用了。
    如果因为类库版本升级而造成了问题,那么单元测试就break了。如果单元测试还是工作正常,那么表示我的代码也可以正常工作。
    当出现问题的时候,我再去拷贝以前的源码出来等等,这样可以让我在相当长的时间里,甚至是“永久”不用去修改这部分代码。
    // 有公开类库这种就不考虑了,如果有的话也不用去hack内部类库。

  42. 徐风子
    *.*.*.*
    链接

    徐风子 2009-08-20 15:42:00

    因为他根本就没有承诺实现任何功能,一切都是你的猜测,对于猜测的东西怎么可能做到完备的测试。

    比如一个加法函数,就只有一个特定需求在35+42的时候返回60(或许这就是他不公开的原因),你怎么可能测出来。

    拷贝全部源码的隐藏意思就是理解里面的内容。

    升级不支持后可以拷贝源码,那如果没有升级,在运行的系统中出现一个问题怎么办?你怎么维护?就像上面那个例子,加一个
    if(expression == "35+42") return 77 else return commo ?

    勿于浮沙之上筑高台,今天的方便会给以后埋下很大的麻烦。




  43. 老赵
    admin
    链接

    老赵 2009-08-20 15:56:00

    @徐风子
    你这个例子举的太极端了,为什么会出现这样无厘头的代码?
    既然决定使用内部方法,也自然是已经理解了其中的功能,怎么能说是“一切都是我的猜测”。
    你认为理解了其中的功能就应该复制出来,我觉得应该直接使用。
    而且根据我的经验,我在.NET 2.0时Hack的代码,在3.5 SP1也可以正常使用,还真没有出过问题。

  44. 徐风子
    *.*.*.*
    链接

    徐风子 2009-08-20 16:18:00

    如果你理解了代码那为什么不直接提出来呢?
    为什么非要hack别人的呢,变成自己的一个library不是更好,大家也可以用。

    例子是有些极端,只是想说明他可能会有一些特殊的需求:比如时间类上他处理了闰秒、在多线程情况下会出错…… (一般说来他既然将一个公用方法私有化,应该是会有一定原因的)

    只是很危险,但不一定会出问题。
    换一种思维,你要是从2.0开始就将那个代码提出来变成自己的library库,那现在是一笔很大的财富了吧,至少很多需要这样功能的人可以直接拿来用,一些开源代码不就这样出来的吗?

  45. 老赵
    admin
    链接

    老赵 2009-08-20 16:23:00

    @徐风子
    因为拿出来的话,会涉及到许多代码,呵呵。
    我Hack代码的时候,往往也是因为要扩展相同的东西。
    例如我现在为什么要Hack这里的ParsedRoute方法呢?因为我就是在扩展Route相关的内容。
    类库在实现Route相关的时候,已经准备了丰富的方法,如果我只是为了这个扩展而去提取出来,我就觉得代码重复了。我喜欢“复用”。

    不过我觉得你提醒得的是对的,对于通用的,可复用的代码,还是不要Hack比较好。
    例如我上次也从WPF的类库内部复制出来一些常用数据结构。有机会我再想想谈谈吧。

  46. Jaxu
    *.*.*.*
    链接

    Jaxu 2009-08-20 17:50:00

    恩,不错,拓宽了思路。

  47. 老赵
    admin
    链接

    老赵 2009-08-20 18:01:00

    @老翁
    接口只有简单几个,但是复制出来可能就涉及到十几个类了。

  48. 老翁
    *.*.*.*
    链接

    老翁 2009-08-20 18:01:00

    没有公开的方法,的确有改动的风险。复制出来不就完了么?呵呵

  49. DiryBoy
    *.*.*.*
    链接

    DiryBoy 2009-08-20 20:00:00

    @Jeffrey Zhao

    Jeffrey Zhao:
    这是什么?搜不到信息啊。



    我的意思是指好像M$的单元测试框架提供的 .accessor

  50. 老赵
    admin
    链接

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

    @DiryBoy
    哪里可以看到资料啊?

  51. DiryBoy
    *.*.*.*
    链接

    DiryBoy 2009-08-20 23:44:00

    @Jeffrey Zhao
    er.. 我只是从“看起来”的角度看的,譬如你写了一个 Xyz 类,里面有些私有成员,不是用单元测试的框架为你生成的 XyzAccessor 类就可以访问了么?

    [TestMethod()]
    [DeploymentItem("Foofoo.dll")]
    public void SomebodyShouldDoSomething()
    {
        SpinWaitLocker_Accessor target = new SpinWaitLocker_Accessor();
        // access private members here……
        target.SomePrivateMethod();
    }
    

  52. 老赵
    admin
    链接

    老赵 2009-08-20 23:58:00

    @DiryBoy
    谢谢啊,我没用过你说的这个……

发表回复

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

昵称:(必填)

邮箱:(必填,仅用于Gavatar

主页:(可选)

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

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

使用Live Messenger联系我