C 是 MVC 的 C

开发桌面应用的时候,model-view-controller 是技术讨论中最为频繁引用的概念之一。所以一直想写篇关于 MVC 的。不过总是犹豫如何入手谈论这个复杂的话题。上周记录了一些学习 OpenGL 的想法,其中一篇和 MVC 有关,有人提出,MVC 是不是 over-engineering ?特别是 controller 这个概念是不是多余的?

质疑一:

没有人觉得这是 over engineering 么? 但凡任何一个把界面代码和业务逻辑代码进行一定程度的分离的程序不都有 view 和 model 么?至于二者之间怎么耦合方法可以多种多样,有必要非得显式设计成什么 controller ,而且还得按照 diagram 上的方式一本正经的互动?

我的意思是说,且不说设计模式是好是坏尚无定论,本来 GUI 编程里需要考虑的方方面面就多,如果 system GUI APIs 还非要强迫你去按照什么设计模式来把 M、V、C 的子类 plug in 才能用就未免太死板了(让我想到了当年失败的 EJB)。

质疑二:

我一直觉得 controller 是个多余的设计。

逻辑层提供 API 给显示层调用,提供(支持 multi-cast 的)callback 机制(callback function、event、或是 ugly 一点的 listener/observer)通知显示层。这样,对于显示层来说,主动被动(或称从上往下、从下往上)的情景都涵盖了,不就够了么,要单独搞个 controller 做甚?

就你说的例子,多个 view 同步可以用注册逻辑层的 multi-cast 的 callback 的确保一致性;异步本身可以封装在显示层内部(也就是逻辑层的 API 是同步的,显示层自己实现异步的假象),不关 model 什么事;drag-n-drop 更是显示层局部的代码互调,也不关 model 什么事。

首先,『如果 system GUI APIs 还非要强迫你去按照什么设计模式来把 M、V、C 的子类 plug in 才能用就未免太死板了』这个观点我认为是无的放矢了。我从来没有看到任何一个 framework 会强迫你按照 MVC 来实现一个程序。不管是 Qt 、Cocoa 、MFC 、Win32 SDK ,你总是可以用很简陋的方式去实现你的 UI 。或者如果头脑够健壮,你完全可以用意大利面条的方式实现复杂的 UI 。『EJB』倒的确是一个经典的强迫开发者实现一大堆 over-engineering 接口的例子,只是除了 Sun 当年会做这样的蠢事之外,这种灾难几乎没有被重复过(即便是同门的 AWT/Swing 也没有像 EJB 强迫接口实现那样强迫 MVC)。

至于说『异步本身可以封装在显示层内部』这种说法,我认为主要基于想象而不是实际的工程经验。实际中,异步操作不可能封装在显示层内部,更不要说很多异步操作还要求显示中间状态,这些中间状态的合法性要靠 model 来判断,否则就不是中间状态而是花屏了。

本文主要回答另一个问题:什么是 controller ,controller 是否必要。我初次接触 UI 开发使用的是 MFC 。MFC 的 UI 开发模式官方称为文档-视图(view-document)—— 没有提到 controller 。像 MFC 这类略过 controller 不谈的 framework 还有一些。

什么是 controller ,实体和规则

从下图表明的定义来看,controller 有三个特点:

  1. 把 UI framework 的底层消息翻译成修改 model 的高层命令;
  2. 设置 view 的状态(比如 active/inactive ,谁是 main view),注意 controller 修改的是 view 的一些简单状态,不能触及 view 的主要显示。其实这是 controller 最弱的一个可有可无的功能。
  3. Controller 只能修改 model ,不能直接修改 view 。View 的修改必须由 model 的通知触发,由 view 本身完成。

上面的第一点可以用来划分在一个 UI 系统中什么是 controller :一般来说,那些直接处理底层消息的回调函数就是 controller 。比如,MFC 里 CViewOnCommand() 。Cocoa 里 NSResponder 的 mouseMove() 。Controller 不需要非得是单独的一个或者几个类。那些认为 controller 是『多余』的质疑往往针对是否需要一个或者几个 controller 类,这是对 MVC 的误解。但是,虽然 controller 无需作为单独的类来实现,它的代码与 model 和 view 的划分还是有明确界限,而且 controller 代码的编写必须符合上面描述的第二点和第三点。

要特别说明的一点是,虽然 controller 不需要非得是单独的类,但是在复杂的应用中单独的 controller 类会带来好处。当 view 的不同实例需要对用户的相同操作做出不同的反应的时候(比如显示缩略图和显示稍大preview 的 view 可以是共用同样的 render 代码,但是会提供不同的操作),单独的 controller 类可以让代码更清晰。

所以,controller 只是告诉开发者如何把消息处理函数实现得更好,更容易维护的一个概念。把它理解成强迫你实现一个模块的 over-engineering 有点强迫妄想的。

Controller 不能做什么

一个通常的对 MVC 的误解是 controller 用来控制 model 和 view 。应该采用的规则是:

  1. Controller 修改 model ;
  2. Model 通知 view 发生了改动,但不能通知改动了什么;
  3. View 自己负责更新可视化效果。

第一个问题是 controller 为什么不能修改 view 。答案是 controller 和 view 都不应该实现 submit change 的逻辑。Controller 的职责是把用户的操作(可以看作对 change 的低级描述)翻译成 model 可以接受的高级 change 描述,但是它不能干涉如何把 change submit 到已有的数据中。View 的责任是如实的表现 model 当前状态(确切的说是最近的合法状态),它也绝不应该涉足把一份增量数据合并到已有的数据集中这样的任务。这样的合并可以很简单,但是也经常很复杂,尤其是对于那些一致性约束很复杂的数据。一个应用应该只有一份代码负责这样复杂的逻辑,那就是 model 。

第二个问题是为什么应该由 model 而不是 controller 通知 view 发生了改变。答案是因为只有 model 知道何时应该通知。如果 model 提供的每个操作都是能原子化的保证数据一致的和同步的,那么 controller 调用这样的操作之后立即通知 view 也未尝不可。但是复杂的应用中,model 的操作往往是异步的、后台进行的、和并发的。如上面提到的,这种异步、后台和并发的特性不但是因为各种客观条件,而且有时是主动设计的。比如耗时很长的操作,又需要展示中间状态,就不能做成一个同步的操作。只有 model 能知道什么是最恰当的时间通知 view 进行更新。

暂态数据能不能由 view 管理

如果一份数据仅仅用于显示临时的可视化效果,能否让 view 直接管理?答案是具体情况具体分析。取决于如何定义暂态数据。比如,有些数据仅仅用于一个操作期间的显示效果,比如拖拽,或者长时间的 cache 更新。问题在于这样的长时间操作期间,UI 并不阻止用户发起其它操作,这些新发起的操作可能和原有操作交替或者同时运行,可能新操作的结果部分或者全部受到原有操作结果的影响,或者新操作可能会终止或者暂停原有操作。而这些复杂操作中可能就包括文档的打开或者关闭操作。

所以,暂态数据不是绝对不能由 view 管理,但是不要轻视 model 的作用。很多时候,让你的 controller 恪守第三条准则『Controller 只能修改 model ,不能直接修改 view 。View 的修改必须由 model 的通知触发,由 view 本身完成』,会让代码的整体复杂度大大降低,也能减少今后必须增加 UI 功能时所面临的引入新 bug 的风险。

什么是 controller

Model 和 view 之间的信息流动必须是有序的。如果假设没有 controller ,那么上面那个图里的五个箭头就必须都画在 model 和 view 之间。如果两个实体之间的箭头有五个之多,这些箭头的端点实际附着的地方肯定是不同的。这些附着点就在 model 和 view 就会形成不同的子模快。所以,除非能证明把这五个箭头中的两到三个是多余的,否则你还是不得不从 model 和 view 里重构出一个实体。只有让实体之间的箭头控制在一到两个,一个 pattern 才真正具有降低整体复杂度的意义。这也正是质疑一里所谓『二者之间怎么耦合方法可以多种多样』的问题所在,对于复杂的 UI 系统,『多种多样』并不是实际可以采取的方法,只是没有实际经验的臆想。

上面对 model 和 view 的讨论说明,这五个箭头缺一不可。所以,model 和 view 模块里实际包含着一些子模块来承担不同箭头的附着点。这些子模块的概念就是 controller 。

Controller 不是一个束缚代码物理位置的约定。它是一个设计的概念,帮助开发者理清信息流动的方向。它是一个刻度,让开发者明白 code base 的行为。你可以说,我的 code base 中 controller 很薄(或者很厚)而且我明白原因。但是如果你说:哦,那是 over-engineering ,我一直都把 controller 这个概念从头脑中清除出去,那你实际上不知道自己在做什么。

发表评论

Fill in your details below or click an icon to log in:

WordPress.com 徽标

您正在使用您的 WordPress.com 账号评论。 注销 /  更改 )

Twitter picture

您正在使用您的 Twitter 账号评论。 注销 /  更改 )

Facebook photo

您正在使用您的 Facebook 账号评论。 注销 /  更改 )

Connecting to %s


%d 博主赞过: