MVC是个老生常谈的话题,不过仍然有必要从剖析这个概念说起。
它既不是一个具体的框架,也不是一种开发环境,那么它是什么呢?其实它是一种“架构模式(architectural pattern)”,与分析模式、设计模式等同属于“模式家族”的一员,其中的分析模式侧重于需求建模,设计模式侧重于具体实现,而架构模式则聚焦于架构设计上。
所谓架构,就是指在一个软件系统的战略级视图,它关注的是分工、协作等管理需求,以及可伸缩性、可扩展性、可测试性、性能等非功能需求。而架构模式就是关于架构设计的经验总结与最佳实践。
需求变更是软件系统的宿命,也是软件系统的价值所在。但是如何去满足不断变化的需求呢?人们给出过很多种方案,而最终流行起来的就是MVC,以及MVC的一系列变种:HMVC, MVA, MVP、MVVM等。这些变种也同样是按照MVC的哲学设计的,只是各个层的“胖瘦”不同。
MVC的核心设计哲学是“隔离变更”。在一个软件系统中,不同的部分之间的“需求稳定性”是不同的。这里的“需求稳定性”是指是否容易受需求变更的影响,通常来说,越是靠近表现层的部分,其“需求稳定性”越低,而越是靠近基础的部分,其“需求稳定性”越高。比如在消费软件领域,命令行界面已经基本退出了历史舞台,但是FAT文件系统却直到今天还有比较广泛的应用。而在软件开发领域,unix/linux系列至今仍然沿用着“微内核架构”,已经足足40年。
模型,简单点说就是“数据”。“数据”的稳定性显而易见,它的生命周期甚至会远远超过用来生成它们的软件。甚至国外一些大公司保存的一些数据产生于上个世纪的五六十年代,几乎和ENIAC一样老。
模型具有“胖瘦”之分。并不是所有模型都只包含数据,很多模型中还包括了对这些数据的操作和校验逻辑。据此,模型还可分为充血模型和贫血模型。充血模型和贫血模型各有优缺点,争论也不少,这里不深入讲解。
通常,模型不会直接访问视图和控制器,如果某个视图需要关注模型的变更,那么就通过模型提供的标准监听接口,把自己注册进去。这样,当模型发生变化的时候,就会主动通知所有关注它的视图,视图就可以更新显示给用户的内容了。
视图,简单点说就是“界面”。“界面”向来变化多端,从传统的字符型控制台界面,到后来的基于操作系统API的客户端界面,再后来的静态HTML界面,以及现在流行的Ajax界面和移动设备界面。但是在这些界面背后的模型和业务逻辑却没有什么变化,甚至这些界面通常还必须共享同一套模型和业务逻辑。
和模型一样,视图也有“胖瘦”之分。有些视图是纯静态的,它可能只有纯粹的HTML模板,一切交互活动都交给控制器执行,甚至连监听模型的变更都要由控制器来负责。而有些视图则是动态的,它包括了与用户交互的逻辑,但不包括业务逻辑。
注意,交互逻辑和业务逻辑不同,它往往与具体的业务无关。比如联动的多级下拉框,它的控制逻辑可以封装成一个独立的可复用模块,控制器只要按照指定的格式给它提供数据就行了,而不用针对每个业务逻辑写一次。现在流行的富文本编辑器也是一种典型的交互逻辑模块。
控制器,简单点说就是“过程”。即:从视图中取得用户的输入,然后把它按某种规则转化为模型的操作,并且把模型的操作结果反馈给用户。这种反馈可能是写回原视图,也可能是把用户导引到另一个视图。
和模型一样,控制器也有“胖瘦之分”,有些控制器同时负责着具体的交互逻辑,有些控制器则同时负责着具体的业务逻辑。
可复用的部分交互逻辑可以转入视图层,这在ajax化的程序中表现最为明显。比如,在angular中,交互逻辑可以由指令(Directive)来实现。
可复用的部分业务逻辑则可以封装成服务,它们可以被多个控制器共享。根据所负责的具体工作不同,有些服务是模型层的一部分,有些则是控制器层的一部分。
Ajax的MVC有点特殊,值得单独剖析一下。
它是一个递归结构。
如果把前后端加在一起看,那么后端就是一个M,而前端的JS程序是C,前端的HTML模板则是V。
如果只看后端,则数据库是M,服务是C,而WebService API(比如SOAP、REST等)则是C和V的混合体,不过这个V主要是给前端程序“看”的,是对机器的“界面”。
如果只看前端,则要取决于不同的框架。以Angular为例:Scope变量是M,模板(Template)是V,控制器(Controller)是C。不过,Angular的视图并不仅仅包括模板,实际上还包括了相当一部分交互逻辑,只不过这些交互逻辑已经由Angular框架实现了。而这些则通过指令(Directive)的形式提供给开发人员。
如果只看指令,那么指令所允许访问的Scope变量就是M,模板(Template)是V,controller函数或compile/preLink/postLink函数是C。