另类MVC模式 - 思考和雏形

这是一篇大逆不道的文章,其作用就是供大家娱乐以及批斗,因为此文所提及的思想,试图改变现有的著名模式MVC的结构,因此如果认为MVC优秀甚至完美的话,还请直接忽略此文,以免影响心情。

本文将提出一种类似MVC但又不完全是现有的经典MVC的模式,该模式仅基于HTTP的Web系统中对经典的MVC模式进行改造,其特点是将View前置,通过View的切分来切分逻辑,形成多次M-V-C交互,最终生成响应

经典MVC模式

对于经典的MVC模式,虽然从表面上看完全是个“不需要解释”的问题,但是每个人的理解又不尽相同。在我的理解中,MVC模式可以用下面这张图来表达:

经典MVC模式

基于HTTP的Web系统里,在经典的MVC模式中,一个请求的处理过程大致分为:

  1. Controller处理原始请求,根据请求的数据与系统的配置,寻找到真正处理该请求的逻辑,称之为Action。
  2. Action处理请求提交的数据,与Model进行交互,获取需要反馈的数据,并将这些数据按View的要求组装后交给View。
  3. 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模式

在这一模式中,请求将有一个不同于经典MVC模式的处理过程:

  1. 请求被称为Locator的组件接收,Locator组件会通过对请求数据的分析,定位到需要的视图。
  2. 视图定义整体页面的框架组织,以及各个区块(Section)。
  3. 视图引擎将解析View,当发现有Section时,进入处理Section的逻辑:
    1. 找到Section定义的对应的Action,并开始执行业务逻辑。
    2. Action与相关联的Model进行交互,取得数据。
    3. 将数据交付到该Action对应的View中。
    4. View输出Section对应的内容。
  4. 视图引擎将页面的框架组织,以及各个Section的输出内容,通过一个缓冲区(Buffer)进行合并,统一输出。

可见,这个另类的MVC模式有自己的一些特点:

  • 请求最先从View开始,而不是Action。
  • 一次请求会对应多个Action,而不是传统意义上的通过一次Action获取全部数据。
  • View是分Section的,最后进行合并。

下一篇将会讲述此种模式的优势及应用场景,并简要地涉及相关的实现方案。