MVC 在演变中被曲解,控制器成垃圾桶,视图沦为半个模型。真正的 MVC 应该让模型独立,界面只是投影。
在软件世界里,MVC 三个字母几乎是人人都听过的术语。无论是写 iOS 的,还是写 Web 的,甚至写桌面应用的,谁都知道这是个“架构模式”。但你可能没想过,这个模式最初的样子,和今天你在苹果文档或者教程里看到的东西,其实完全不是一回事。
如果说Smalltalk里的 MVC 是一朵开在计算机科学花园里的白莲花,那么今天大多数人理解的 MVC,已经变成了一个塞满逻辑的垃圾桶。
四十多年前,一群天才在施乐帕克研究中心捣鼓出了一个叫 Smalltalk 的玩意儿:正是在这里,MVC 作为一个清晰、优美、充满哲学思辨的设计模式横空出世。
它的核心思想朴素得令人发指:模型是你的数据和业务逻辑,视图是数据在屏幕上的样子,控制器则是用户输入如何影响模型和视图的翻译官。
三者泾渭分明,模型根本不知道视图和控制器的存在,它只管自己的事儿,通过“通知”这种间接方式告知外界“我变了”。
在 1995 年的《设计模式》里,艾里希·伽玛这样定义 MVC:模型是应用对象,视图是屏幕上的表现,控制器是用户和界面的桥梁。
最关键的一点是,模型完全不知道视图和控制器的存在,它只管通过通知机制抛出变化。
于是模型天生可以复用,因为它和界面毫无耦合。这种设计,让模型代码具备了极高的可重用性,这才是 MVC 的灵魂。
苹果的“扭曲”:控制器成了垃圾桶
当苹果公司在八十年代初,那群从施乐挖来的牛人开始打造 Lisa 和后来的 Macintosh 时,问题就出现了。
Smalltalk 是一门纯面向对象的语言,万物皆对象,连一个布尔值都能被观察;但在当时主流的 Pascal 或 C 语言里,想实现一个“可被观察的布尔值”?那简直是给自己找罪受,你得操心对象的生命周期、内存管理、绑定表达式,麻烦得要死。
再加上早期硬件性能孱弱,大家潜意识里觉得一个界面元素就该对应一个数据模型,搞那么通用的抽象太“浪费”了,于是,为了“实用”,为了“效率”,第一代苹果工程师们做出了妥协,把 MVC 的精髓给阉割了。
这种妥协像病毒一样,随着 Mac 和后来的 iOS 生态的壮大而疯狂传播。
苹果官方文档里曾经赤裸裸地写道:“控制器通常是应用中最不可重用的对象,但这没关系。” 这句话简直是给后来所有烂代码颁发的免死金牌!它暗示开发者:控制器嘛,就是个大杂烩,把所有搞不定的逻辑、网络请求、数据解析、动画控制、甚至业务逻辑都往里塞就对了。
久而久之,“ViewController” 不再是一个协调者,而变成了一个无所不包的“上帝对象”,一个塞满了各种私有方法和临时变量的巨型垃圾场。你去看任何一个稍有规模的 iOS 项目,那个动辄几千行的 ViewController 文件,就是 MVC 被玩坏的活体标本。
最核心的背叛,在于对“模型”二字的彻底遗忘。
在 Smalltalk 的原教旨 MVC 里,模型是绝对的主角,它是一个可以被观察的对象,它的每一个状态变化都应该能被清晰地通知出去;视图的工作极其单纯:拿到通知,然后把自己更新成模型当前状态的样子。
你永远不会去问一个复选框“你现在是选中还是没选中”,因为它的状态就是模型状态的直接映射。
但在我们今天的实践中呢?我们习惯于去查询一个 UI 控件的值,监听它的点击事件,然后再手动去更新数据:这本质上是把模型塞进了控件里,是本末倒置!这种模式在简单场景下还能凑合,一旦业务复杂,数据联动一多,立马陷入“我改了A,B没变;我改了B,C又错了”的无限循环,代码的可维护性直接归零。
更讽刺的是,我们连“模型”是什么都搞不清楚:我们能识别出“用户模型”、“订单模型”这种大块头,却对那些细微的、无处不在的“模型”视而不见。
比如,一个窗口的可见性、一个列表的当前选中项、一个图片编辑器的撤销栈,这些都是模型!它们都应该被独立出来,被观察,被管理。
特别是“函数参数模型”,这是被忽略得最彻底的:你界面上有个“删除选中图片”的按钮,这个按钮能不能点,取决于“当前选中的图片集合”这个模型是否为空。
这个“选中项集合”就是一个独立的模型,它有自己的状态(空或非空),这个状态的变化应该能被按钮观察到,从而自动禁用或启用。而不是在按钮的点击事件里,再去临时判断一下选中项。
这种对“模型”的漠视,直接导致了我们的业务逻辑散落在各个控制器的犄角旮旯,变成了无法复用、难以测试的“附带类型”。
所以,MVC 被玩坏,不是模式本身有问题,而是我们在工程实践中,为了眼前的“方便”,放弃了对架构纯洁性的追求,忘记了“高内聚、低耦合”的祖训,把控制器当成了垃圾桶,把模型当成了摆设。
模型才是 MVC 的心脏
想真正理解 MVC,必须先弄明白模型是什么。模型就是一个能被观察的对象。
举个最简单的例子:假设模型只是一个布尔值,当控制器请求更新,它变了,然后发出通知,视图再去刷新界面。模型不知道它被绑定在一个复选框上,也不知道界面长什么样子,它只负责做自己。甚至多个界面可以同时绑定在同一个模型上。(banq注:是不是模型替代控制器做上帝对象呢?)
这才是数据绑定的本意。但很多框架做反了:它们让你去问控件的状态,而不是直接观察模型。于是才会有那种奇怪的写法——问复选框是不是勾上了,而不是看布尔值是不是 true。这样一来,责任混乱,扩展性差。
在 Objective-C 中,一个基本数据类型(如 int)本身不是一个对象,因此不具备内置的通知机制,它是不可观察的。然而,当一个 int 类型作为属性被封装在一个 Objective-C 对象中时,该对象可以利用键值观察(KVO)机制来使这个属性变得可观察。
模型的作用在于能够封装属性之间可能存在的复杂逻辑关系:一个最简单的模型形式,是其中所有属性都彼此独立,互不影响,这种模型在概念上类似于 C 语言中的结构体或一个极其简单的 C++ 类。
当模型发生变化并通过通知机制告知视图时,视图至少应当能够从通知中解析出两个关键信息:
1. 发生了什么变化? 在某些实现中,这个信息可能非常笼统,例如仅仅是一个“模型已更新,需要刷新”的泛泛信号。
2. 要显示的新值是什么? 视图需要获取更新后的具体数据以便正确显示。
举一个最简化的例子:假设我们的模型是一个包装了布尔值的可观察对象。我们的目标是将一个复选框(UIView)绑定到这个可观察布尔值上。当应用程序的控制器(Controller)请求改变这个值时,模型内部的布尔值会更新,紧接着模型会主动将自己的新状态广播给所有观察它的视图。关键在于,模型完全不知道、也不关心究竟是哪个具体的 UI 控件在观察它。理论上,同一个模型实例甚至可以同时被多个不同的 UI 组件(乃至非UI系统如脚本系统)所观察和订阅。
大多数 UI 框架都有一个复选框窗口小部件,您可以从中查询值,并在值发生变化时收到通知。这是将模型推送到窗口小部件中。
使用 MVC,您永远不会问“这个复选框的默认状态是什么?”这样的问题,因为视图的默认状态始终是模型的当前状态,您也永远不会获取复选框的状态,复选框的状态只是模型状态的反映。
MVC 其实是组合模式
最后还有一点常被忽视:MVC 不是简单的三块拼图,它其实是一种组合模式。
视图本身也可能有状态,这些状态也是对象,也是模型。比如窗口是否可见、用户上次切换到哪个标签页,这些都应该建模。
理解这一点,才能避免视图和控制器无限膨胀,把系统拖进泥潭。
控制器和视图之间的直接连接暗示了这一点:正如我之前所说,视图可能包含状态,该状态本身是一个对象,并且由于该状态也显示在视图中,因此是可观察的。它是另一个模型。我将其称为视图的模型。该模型可能包含诸如窗口的可见性、用户上次查看的选项卡以及正在查看的模型部分等内容。
识别系统中的模型非常重要:
我们通常很擅长识别主要模型,例如“这是一张图片”;
但往往无法识别完整的模型,例如“这是一张带有一系列设置的图片”。
最终,我们的模型散布在代码中(一种偶然的类型),这使得处理起来更加困难。
一个经常被完全忽视的常见模型是函数参数模型。当你的应用程序中有一个命令、按钮、手势或菜单项时,它们都会绑定到一个函数。函数本身通常不是零元函数,而是包含一组通过 UI 其他部分构造的参数。
例如,如果我的应用程序中有一个图像列表,我可能有一个按钮来删除选定的图像,这里,当前选择就是我的删除命令的参数。
要为选择创建 UI,我必须创建函数参数的模型,删除的一个前提条件是选择不为空,这个前提条件必须在参数模型中可观察,这样它就可以通过禁用或隐藏按钮反映在视图中,并在控制器中反映在用户点击按钮并发出命令。
同一个参数模型可以在一个应用程序中的多个命令中共享。
banq注:
识别上下文Context情况,就能识别出模型,凡是有特别情况的地方就需要模型,但是对上下文场景的感知不同人敏感度不同,很难达成共识,因此,将模型看成是一种数据,反而是共识,这是大家都这样 离谱即常态 是最危险的谎言 ,这是一种行业内的术语共识,外行人也许觉得是畸形,其实可能就是专业语境造成的行业壁垒,否则这个行业高管专家为何年薪几百万?难道他有什么不同之处吗?唯一不同就是他掌握了你不懂的行业术语共识:什么是低语境、高语境?
作者认为:MVC 为什么会被搞得面目全非?一是语言的限制,二是偷懒的妥协,三是概念传播中的曲解。Smalltalk里的 MVC 是优雅的分工,模型独立、界面投影;而今天常见的 MVC 却成了混杂体。
或许我们该重新想想:在自己的代码里,模型是不是独立的?视图是不是真只是投影?如果答案是否定的,那你的 MVC,可能已经变了味。
作者背景:本文整理自一位长期研究软件架构与设计模式的技术专家的思考。他早在 2003 年就批评过苹果对 MVC 的误导性定义,之后持续关注编程语言、对象建模与架构模式的演变。他的洞察带有历史深度,也直击现实痛点。