另类MVC模式 - 思考和雏形
这是一篇大逆不道的文章,其作用就是供大家娱乐以及批斗,因为此文所提及的思想,试图改变现有的著名模式MVC的结构,因此如果认为MVC优秀甚至完美的话,还请直接忽略此文,以免影响心情。
本文将提出一种类似MVC但又不完全是现有的经典MVC的模式,该模式仅基于HTTP的Web系统中对经典的MVC模式进行改造,其特点是将View前置,通过View的切分来切分逻辑,形成多次M-V-C交互,最终生成响应。
经典MVC模式
对于经典的MVC模式,虽然从表面上看完全是个“不需要解释”的问题,但是每个人的理解又不尽相同。在我的理解中,MVC模式可以用下面这张图来表达:

基于HTTP的Web系统里,在经典的MVC模式中,一个请求的处理过程大致分为:
- Controller处理原始请求,根据请求的数据与系统的配置,寻找到真正处理该请求的逻辑,称之为Action。
- Action处理请求提交的数据,与Model进行交互,获取需要反馈的数据,并将这些数据按View的要求组装后交给View。
- View根据Action递交的数据,组装为用户可识别的视图模板,并通过响应传递到客户端进行渲染。
遭遇问题
这个模式一直工作得非常好,我也完全没有理由反驳他存在的意义和优势,直到有一天我发现我的页面有了些奇怪的需求:

页面也是一个经典的“个人日志”的页面,只是加入了一些SNS的元素,如右边有“最近来访”及“好友列表”模块,同时还有一个标签云。
对于此类系统,右边的模块往往会较为通用,但又随着页面内容的不同而有所变化。例如在“个人资料”页面,则不会有“最近来访”的模块,取而代之的是加入了“关注人物”模块,这种既有稳定性,又有动态性的模式无疑是设计的一大难关。
通常来说,如果我使用的是ASP.NET MVC框架,当出现这样的需求时,会创建一些Action Filter,用于集中各个模块的逻辑,随后将数据放入ViewBag这一属性中。得益于.NET 4.0提供的dynamic数据类型,可以在不定义Model基类的前提下将此类通用的数据传递给View。
随后只要在Action上声明需要哪些模块的数据即可:
[Friends]
[RecentVisitor]
[TagCloud]
public ViewResult ViewPost(int id) {
// 获取日志数据
}
从设计上而言,我认为这是一个合适的方式,通过AOP的方式抽取了一些公共的数据,交由View进行组装,符合传统的MVC模型,且不造成过多的冗余逻辑,直到有一天,我又遇到了一个问题:
由于系统希望引入异步模型,更好地利用多核及IO资源,并行地去获取各项数据,因此顺序执行的Action Filter不能再满足场景。
也就是说,我们希望通过将MVC中Action与Model的交互进行一些改造,将彼此没有强烈联系的各项数据的获取过程并行化:

这种方案是对Model组件的修改,Controller对应地进行配合,保持response对象直到所有数据以异步的方式获取完毕,一次性递交给View即可。这显然是一种可行且简便的实现,例如.NET 4.0的Parallel库就能很轻松地完成这一工作。但是在看到并应用这种手段时,我却不由得想到一个问题:
“好友列表”或者“近期访客”这样的模块,和日志本身有什么关系?
进一步的:
如果没有关系的话,为何一个Action需要处理这些数据(无论是通过Action Filter还是硬编码)?
在不断反思这个问题的同时,又会有这样的想法:
为什么一个响应只能由一个Action处理?明明不相关的几个区块,是不是由多个Action处理,形成多个View,再组装起来更合适呢?
现有方案
想到这一步,我就发现有1个常见的技术经常被用来处理此类页面,即“AJAX延迟加载”。在这种方案下,最初输出的View只包含对应模块的结构和容器,随后通过AJAX请求读取各模块的视图内容,由前端负责组装。
这种方案保证了页面的主要内容第一时间被送到客户端,也保证了各模块的逻辑(Action)互不重叠、冲突,实现干净利落的切分和隔离。但其代价是产生多个HTTP请求,在请求-响应级别的优化能力有限。
为了将HTTP请求数量进行缩减,此后又出现了BigPipe技术,虽然并没有改变经典MVC的结构,但做到了将“模块化页面”压缩至一个HTTP请求之中。同时BigPipe还能够更有效地利用浏览器的资源,让页面的渲染和内容的下载并行进行,对于复杂的、对浏览器渲染时间要求较高的页面有奇效。
综合分析了现有技术,却发现并没有理想的解答自己提出的问题。AJAX延迟加载虽然对Action进行了切分,却导致了HTTP请求过多等负面效果;而BigPipe并不是对逻辑处理的切分,只是一种HTTP响应输出技术,只能提供些许思路,却没有办法从根本上解决问题。
我的方案
于是又经过多天的反思,以及和一些朋友的讨论,最终我发现自己走进了一个“误区”:
传统MVC模型的请求由Controller开始,依次经过Action->Model->Action->View,最终变为输出。但这并不代表着此流程一定是“完美”的。
也许这个我认为的“误区”,对多数人来说是“真理”,因此要推翻这一点,确实得抱有足够的勇气……
首先,无论我们使用什么样的技术手段,对于用户来说,我们的页面是怎么样的:
- 主要内容为日志的标题和信息。
- 有一块显示最近来访的用户。
- 有一块显示作者的好友。
- 有一块显示标签云。
抛开底下的技术不谈,如果从视图和功能上进行划分,这显然是4个区块。但我们又是如何发现这是4个区块,而不是2个或者6个呢?答案是:在网页上用眼睛看。
是的,无论用什么样的技术,用户肉眼之所见,对他们来说永远是第一位。那么,为什么在逻辑上,却将供肉眼所见的“视图”放在最后呢?当然“因为一出来就给用户看了,当然要最靠近用户”这样的理由是成立的,但又为什么“用户一请求就应该收到了,因此要放在最前面”呢?
在这个近乎变态的想法的支撑下,我试图将View作为请求的入口,通过View的切割来产生不同的区块(Section),而不同的区块对应着不同的逻辑(Action<->Model),又产生属于区块自己的视图,最终合并为整体:

在这一模式中,请求将有一个不同于经典MVC模式的处理过程:
- 请求被称为
Locator的组件接收,Locator组件会通过对请求数据的分析,定位到需要的视图。 - 视图定义整体页面的框架组织,以及各个区块(Section)。
-
视图引擎将解析View,当发现有Section时,进入处理Section的逻辑:
- 找到Section定义的对应的Action,并开始执行业务逻辑。
- Action与相关联的Model进行交互,取得数据。
- 将数据交付到该Action对应的View中。
- View输出Section对应的内容。
- 视图引擎将页面的框架组织,以及各个Section的输出内容,通过一个缓冲区(Buffer)进行合并,统一输出。
可见,这个另类的MVC模式有自己的一些特点:
- 请求最先从View开始,而不是Action。
- 一次请求会对应多个Action,而不是传统意义上的通过一次Action获取全部数据。
- View是分Section的,最后进行合并。
下一篇将会讲述此种模式的优势及应用场景,并简要地涉及相关的实现方案。
已有2个评论发表评论
这个。。“View是分Section的,最后进行合并”。。。。就是我一直想要做的。。。 我的想法是。。,想把section做成mef式的。。。只要按照特定的方式写好,放到某文件夹中,就可以自动加到特定的区域,比如右边的区域的无限扩展。。。在我项目里面的意义则是用于扩展已有的数据呈现引擎,较自由的实现用户定制需求,比如在某些模块新加按钮,新加数据展示区之类的,
。。。e。。。。这个。。。原来不是我说的那个