在ASP.NET平台上实现MVC框架
后记:本文发出後我也见到了一些号称在ASP.NET平台下实现MVC的例子,不过我看到的例子都没有很好的与ASP.NET兼容,更多的,他们是一种另起炉灶的方式。坦白的将,另起炉灶实现一个MVC Web框架并不难,因为Web框架的做法都已经基本成为定式,有很多平台的成功MVC框架可以参考。但是,这里我们实现的这个与其他的有所不同,因为我们并不想重新发明轮子。我尊重MS的ASP.NET框架,虽然她有各种各样的缺点,但显然她很好实现了自己的目的。作为一个合格的开发者,所要做的就是结合成本提供一个最合适的解决方案,显然,我们尽量不必一切推倒重来。
7/22/2007
一、 表现层的重要性
表现层从系统架构设计的角度上讲,永远不是最重要的。系统架构师们往往会更加关注处于核心层次的东西,大家总是想在稳定核心层次之后再去深究表现层的东西。我想,从项目管理的角度讲,这点是毋庸置疑的。我们势必会按照风险的高低,需求的重要性来对项目需求进行分类,从而率先解决最核心,最基础的问题。
但这也并不表明我们就可以忽视表现层,将其放置於一个不置可否的境地。有两个例子都可以反映表现层的重要性。
l 表现层历来都是兵家必争之地,无论是GUI时代还是Web时代。表现层曾经被忽视,但是在Google 和Ajax风行后的今天,各大厂商又开始了对表现层的争夺。在以微软的XAML为代表的若干“*XML”预先出现之后,表现层之争愈演愈烈。
l 表现层其实是与用户接触最为密切的地方,正所谓系统的接口。我们都知道用户需求是最重要的,而用户只有使用我们的表现层使用系统,所以表现层是我们的系统能否满足用户需求的直接表现。
显然,从这个角度讲,表现层的功能直接与用户理解息息相关,其重要性不言自明。
二、 惯用的WEB层设计思想
谈表现层显然会提到MVC,如图1所示。MVC的最早发明者是Trygve Reenskaug,他最早被应用在Smarttalk系统中。也许Trygve Reenskaug并不会想到今天MVC的重要性,它显然已经是表现层模式中的法宝。如果没有MVC,那人类的软件革命也许会倒退若干年说不定也有可能。很难想象一个Model和View严重耦合的系统是多么的难以维护。
图1、MVC图示
MVC说到底其实是一个组合模式,他综合了Strategy,Observer和Composite四个模式。其中Strategy和Observer特别重要,他们分别将View与Controller,View与Model解耦,从而实现了View和Model的复用。而MVC最大的贡献,也就是讲不同界面所带来的复杂点集中在Controller一点上,从而解放了View和Model。将系统的变化和复杂点集中于一点,是人类在进行软件设计时所坚持的一个基本原则。
表现层走到今天,显然已经是Web的天下,Google的出现甚至让微软都如坐针毡。MVC模式在Web领域也有了自己的发展,这就是所谓的Model 2。关于Model 2与传统的Model 1有什么区别,你可以很轻松的从这两张图看出来:
图2: JSP Model 1 architecture
图3: JSP Model 2 architecture
显然,有经验的Web开发者都还可以记忆起那段日子,那段HTML标签和程序逻辑代码混淆的日子。Model 1正是那段日子的产物。严格说来,按照Martin Fowler的划分,Web View技术主要有两种,一是Template View,另一个是Transform View。JSP就是典型的Template View,而Transform View主要是指类似XSLT这类的。我们可以将构成一个页面的逻辑分为两种,控制(Control code)代码和格式化代码(Formatting code)。如果将JavaBean理解为数据的话,显然在Model 1里,控制代码和格式化代码都被写在JSP中。Model 2就会好一些,因为他已经将控制代码分离出JSP,用Servlet形成了一个独立的Controller。一个很好的习惯就是,永远让View只做格式化的工作。
Web上的MVC是特殊的,与GUI上的会有不同。毕竟HTTP是基于保守的请求-相应模型的,View被简化为一种称作HTML的标记语言,客户端的相应演化为对字符串的解释。所以,在Model 2中,Controller演变成了输入控制器(Input Controller),他负责对入参进行分析,然后调用相应的Model读取数据,并且将数据传递给View。
如此这样,View就会被限定为只做格式化部分的工作,而对数据的调用部分有Controller去做。当View不了解Model的信息时,显然它是可以被复用的。Model是Domain领域模型,显然他也可以被复用的。而Controller集中了所有的变化信息,他的逻辑反映了用户需求的变化。
当然,一个完整的Web框架的设计不仅仅只有这些。MVC只是一个理念,但具体如何实现呢?
图4、一个典型的MVC Web框架的设计
框架实际上是一个权衡,如何在保持最大灵活性的情况下,做到最多的工作。框架还有一个隐含的意义往往被人们忽视,那就是定制系统结构,将好的设计潜移默化到框架的使用当中。所以,在MVC框架的设计中,一般会对框架进行角色设计,从而能够将好的设计贯彻倒框架使用中。
在通常的MVC框架中,一定会有类似的角色,那就是Action Controller,View, 还有Front Controller。用户通过继承Action Controller来为每个不同的页面创建不同的Controller,而View则一般是对Template View的抽象。Front Controller是这里的关键,它由Router,Dispatcher,Plugin等角色组成。所有的访问都回首先被Front Controller处理。Router负责解析URL,拆分出有用的信息,而Dispatcher根据Router拆分出来的信息选择相应的Controller进行处理。Plugin这里主要是给用户提供了一定改变Front Controller行为的能力。用户可以实现自己的Plugin,从而改变Front Controller的行为。系统的工作原理可以如下图所示。
图5、典型MVC框架的通讯图
了解了惯用MVC框架的设计,我们可以轻易发现现在的View和Model被成功的分离开。通过中间的Controller将View和Model连接起来,View和Model之间的链接就可能有无数的可能。只要Controller愿意,它可以根据任意情况去组织,系统内部的代码得到了最大的重用。
三、 ASP.NET的缺陷
OK,上面了解了很多东西,但是这些和ASP.NET有何相关呢?坦白的说,上面的设计你可以在很多有名的平台上看到,比如Java,PHP或者是Ruby,不过ASP.NET却没有这么幸运。
在微软的ASP.NET平台上,世界是这样的:
图6、ASP.NET的结构
HttpApplication是这里的主角。ASP.NET在接收到一个请求后会初始化一个HttpApplication,由他来进行处理。而HttpApplication会根据请求的需求,寻找相应的IhttpHandler来进行处理。IhttpHandler在掌握到控制权後,可以通过HttpContext,HttpRequest,HttpResponse对象来获得所有输入信息,进行输出。
我们知道,一个ASPX页面其实也就是一个IhttpHandler,它具备ProcessRequest接口,从而能够相应HttpApplication的请求。微软为了解决Web编程固有的几个问题,对ASPX页面进行了设计,将页面拆分为若干控件,并且定制了服务器端页面模型,以及极为复杂的组合模式和事件机制,从而实现了若干目标,比如减少Web开发所需要的技能(屏蔽Javascript),将代码和标签分离(所谓的后台代码)。微软成功的实现了他的目标,但是他的这个设计也带来了问题。
还记得我们的分析吗,一般一个页面总会有两种代码,即控制代码和格式化代码。控制代码负责分析用户输入,根据用户的输入存取Model,然后将数据交给界面去做格式化。既然ASPX本身就是一个IhttpHandler,那么它本身就要负责所有的工作。也就是说在ASPX页面中集合了两个角色,它们分别是Controller和View,一个负责控制逻辑,一个复合格式化逻辑。这还不算什么,微软还把ASPX的模板页面和他的后台代码紧紧绑定在一起,从而让Controller和View之间出现了紧耦合,造成了View的不能复用。如果要制作ASPX页面的话,你会碰到如下的尴尬:
l 格式变化,你却必须重写你的Controller。格式变化了,也就是说你的模板变化了,于是你不得不重新写作一个ASPX页面。也许你会将原来的后台代码复制过来,但显然,你不能直接的复用他。
l 控制逻辑变化,你却必须重写你的View。控制逻辑变化了,View没变,但是没有办法,你的View和Controller是绑定的,所以你的View还需要重写,也许又是代码拷贝后的修改,谁知道呢。
问题是你要知道复制粘贴永远不是代码复用的好习惯。现在你是可以复制粘贴,如果日后有修改,那么你会面临成百上千的文件需要修改,你还高兴的起来吗?
也许你会想,那我可以把Controller的通用功能抽象出来,建立成共享的类供大家调用,从而最大程度上的增大可复用的代码。可问题是耦合还是耦合,你最然提高了重用,但是并没有减少依赖。当一个需要修改很多依赖的需求变动发生时,可能你修改的代码很少,但是他们却分布在非常多的文件里。
更重要的是,框架是一种好思想的倡导。当框架没有倡导一种好的结构时,程序员们往往喜欢滥用,这点你看看ASP.NET开发出来的程序就会发现。而当我们将好的设计思想揉入框架的实现时,程序员们也会自然而然的继承我们的思想,从而为系统带来一个良好的结构。君不见多少使用Struts的初级程序员也可以写出符合MVC模式的程序来,框架倡导的作用可见一斑。
四、 我们如何改造ASP.NET
我们并不否认ASP.NET是一个非常伟大的Web框架,原因就是他确实解决了一些一直困扰Web开发的顽疾。可微软似乎做过了一点,但如果不这样似乎也不是微软的风格。
尽管ASP.NET并不是仅有只有这么一点缺点,但显然架构上的问题是非常重要的。一方面,我们知道在开发时,重用是非常重要的。重用最重要的并不是在于他可以节省代码,而是在于它可以解除偶和。也许你的只有100行冗余代码,但如果这些代码分布在100个文件或者类中,那将是无法接受的。所以在进行软件设计时,我们总是想尽办法将耦合或者冗余集中、减少,而且集中永远比减少要更加优先。我们宁愿存在一个1000行代码一个耦合的类或者文件,也不愿意我们的耦合分布在100个点,而每个只有一行而已。
试想,如果我们有种方法能够提ASP.NET解耦,将他的Controller与View分开,那么我们就可以重用View,并且将所有的耦合和变化集中在Controller中,这样是非常理想的。但问题是微软平台上你从来不会获得那样的灵活性。首先,微软不会开放源代码让你修改;其次,ASP.NET是一个严重的Event Driven的模型,与多数MVC框架的Action Driven模型有所不同。微软很喜欢用事件,而我也不想重新发明轮子。
显然,还是有办法的。
其实,微软ASP.NET的框架设计还是非常灵活的,他为用户的扩展提供了非常多的空间,需要的只是理解原理而已。对于图4所示的MVC架构,需要的只是一个突破ASPX的限制。我们尊重微软ASP.NET的设计,我们也坚持不要重新发明轮子的原则,将微软的ASPX放置到View的层面上,主要负责数据的格式化,界面控件的事件相应等。而我们采用HttpModule来调用我们的MVC架构,从而实现在View层之外调用控制代码的能力。
显然,我们可以有HttpApplication和HttpModule两种选择从而实现MVC,但是HttpModule具有更大的灵活性,因为当你不需要他的时候,只需要从配置文件中去掉相应的HttpModule就可以。
五、 结论
每一个平台都是伟大的,ASP.NET完成了一些令人赞叹的特性,为Web开发作出了出色的贡献。但是每个平台处于种种限制,总是有他的局限性。当微软需要实现更加快速,更加简单的Web开发时,他不得不让架构做出一点牺牲。这也是为什么微软的ASP.NET平台不太适合企业开发的原因。
可是作为一个开发者,我们有时候需要抛开平台的限制,从更高的层次上考虑系统结构,从而克服某一特定平台的限制。显然,一个成熟的平台都有足够的通用性,如果开发者能够从一个更高的层次去考虑,我们将获得更多的选择。
Coofucoo Zhang
7/14/2007