GUI 架构简述

本文从细节入手,尝试分析了几种常见的 GUI 架构:MVC、MVCS、MVP、MVVM。对于在实际开发中如何选择给出了一些参考意见。

©原创文章,转载请注明出处!

Overview


移动开发架构,无论是 iOS、Andriod 还是 Web 都属于 GUI (Graphical User Interfaces) 架构范畴。2006年,Martin Fowler 的 GUI Architectures 一文可谓是经典之作。文中 Martin Fowler 提到 MVC 模式如何组织代码、划分模块职责,还提到 Data BindingFlow Synchronization 以及 Observer Synchronization 等核心概念。
纵观十年来 GUI 架构演变,无论是 MVCS、MVP 还是 MVVM,其实讨论的核心问题还是如何分层、如何划分模块职责、做好代码隔离。

谈到 iOS 上常见架构,相信只要有半年以上开发经验的同学都能侃侃而谈。但在实际交流过程中发现不少同学对关键细节问题却认知模糊,甚至是错误的。因此,本文尝试从细节入手对几种常见架构进行简单描述(对架构的认识智者见智、仁者见仁,我所描述的也不一定是正确的)。

为什么要分层


上文提到各种架构虽各种不同,但它们其实都是在讨论一个问题:『如何分层』。
那么在继续之前,我们有必要思考一下:为什么要分层?

计算机界有一句大道至简的名言:

All problems in computer science can be solved by another level of indirection.

之所以要分层,最终目的是降低系统整体复杂度。
通过分层我们至少能获得以下能力:

  • 提供良好的抽象,隐藏实现细节,降低耦合度;
  • 隔离变化;
  • 提高模块可复用性;
  • 增强系统可扩展性。

说到分层,可能最先想到的例子是 OSI 七层或 TCP/IP 五层网络模型:
在分层网络模型中,不同协议工作在不同网络层,互不干扰,又协调有致,如:IP 协议工作在网络层,主要职责是网络寻址;TCP 协议工作在传输层,主要负责建立可靠的网络连接、负责拥塞控制等。正是因为良好的分层,IP 协议无需关心 TCP 协议的工作,反之亦然。同时 IP 协议也可以在 TCP、UDP 协议间复用。

如今对网络安全越来越重视,HTTPS 协议也慢慢普及,通过分层,只需在原有 HTTP 协议基础上添加一个 TLS/SSL 的安全传输层即可,由它来负责加解密,而原有的 HTTP、TCP 协议无需修改:

Model-View-Controller


MVC(Model-View-Controller)作为最经典的架构,广为人熟知,也是 Apple 官方推荐的移动架构。
MVC模式的核心思想是数据层(Domain)与表现层(Presentation)的隔离。

Separated Presentation:
Ensure that any code that manipulates presentation only manipulates presentation, pushing all domain and data source logic into clearly separated areas of the program.


那么,在数据与展现被隔离之后,它们之间如何同步数据、状态?
这就涉及 MVC 模式另一个重要思想:观察者同步(Observer Synchronization)。

Observer Synchronization:
Synchronize multiple screens by having them all be observers to a shared area of domain data.

常用方法:在Presentation Object(Controller)中注册通知、设置delegate、传递block等。当数据需要更新时,Domain Object(Model)通过上述方式将数据自底向上的同步给Presentation Object。

下面简单介绍一下 Model、View、Controller:

Model


Apple: Model Objects Encapsulate Data and Basic Behaviors.
Stanford: Model = What your application is (but not how it is displayed).
简单讲:Model = Data + Manipulate Data

(ps:本文中的Stanford表示斯坦福的 iOS 公开课)

如:我们书架的 Model:QRBookShelfModel,包含了数据:NSArray<QRBookShelfItem *> *books以及对数据的操作:addBook:deleteBook:等。

Controller


Apple: Controller Objects Tie the Model to the View.
Stanford: Controller = How your Model is presented to the user(UI logic).

Controller 是 Model 与 View 间的连接器,其核心职责有:

  • 处理用户事件;
  • 处理展示逻辑;
  • 连接 Model 与 View。

这里有个问题:到底什么是展示逻辑?
简单讲:将业务数据转换成UI数据,如:

  • 下载进度,从 Model 层返回的是 double 型,将其转换成可展示的 string 类型(0.811—>81.1%);
  • 性别,从 Model 返回的是0、1这样的 int 型,将其转换成:1->男,0—>女;
  • 日期,将时间戳格式化:123456789923—>2016-07-01 10:09.

View


Apple: View Objects Present Information to the User
Stanford: View=Your Controller’s minions

总之,View 只做一件事:layout。

MVC 规则


为了实现 MVC 的核心思想:业务 (model) 与展示 (View) 的隔离,必须严格遵守一些规则:

  • Controller 依赖(持有) Model、View(可直接与它们通信);
  • Model 与 View 互不可见(不可通信);
  • View 只负责layout,且不能保存业务数据 (需要数据时通过 datasource 方式向 Controller 要);
  • View 可通过 target、delegate 与 Controller 同步状态;
  • Model 不能主动与 Controller 通信,通过 Notification、KVO、delegate、block 等机制通知 Controller 数据变化。

看到这里,大家有没有一种熟悉的味道?
没错,UITableView 与外界 (Contoller) 的交互与此处的描述高度一致。

(关于 MVC 规则的描述,大家也可以参考 Stanford iOS 公开课中的相关内容)

有问题吗?


此时,大家或许心中有些疑问:
1、在 MVC 模式中,网络请求、数据存储谁来完成?
2、Model、View、Controller 谁的可复用性最强?
3、展示逻辑为什么由 Controller 完成而不是 View?

  • 在 MVC 模式中,网络请求、数据存储谁来完成?
    Controller、Model 都可以,一般由 Model 完成。此时的 Model 已不再是简单的 Model Object,而是 Model layer,在我们项目中通常将其称为 Manager。

  • Model、View、Controller 谁的可复用性最强?
    View>Model>Controller

  • 展示逻辑为什么由 Controller 完成而不是 View?
    View可复用性高,不应关心具体展示逻辑,只专注于 layout

Massive View Controller


MVC 模式被批评最多的就是 Controller 过于臃肿,那么 Controller 都做了什么?

  • 处理复杂的展示逻辑;
  • 处理用户事件;
  • 初始化 View、管理部分 View 的生命周期并提供数据;
  • 处理业务数据变化,转换为 UI 结果;
  • 获取、存储数据(可选)。

尤其是如今很多产品经理『擅长』做加法,页面、交互越来越复杂,这对于 Controller 来说无疑是雪上加霜。

Model-View-Controller-Store


前面提到,在 MVC 模式中,并没有讨论获取数据属于哪个模块的职责 (一般由 model 负责)。MVCS 模式就是在 MVC 基础上将数据单独提取为一层(Store)。

Model-View-Presenter


在 MVC 模式中,展示逻辑被划分为 Controller 的职责范围。如今,展示逻辑越来越复杂,Controller 随之也变得越来越臃肿。同时,Controller 也被认为是 View 的一部分,这样 Model 与 View 间并没有完全隔离、解耦。
MVP (Model-View-Presenter) 就是在这样的背景下产生的,其将展示逻辑提取为一个单独的层(Presenter),简化了 Controller,也彻底隔离了 Model 与 View。

新产生的 Presenter 层有以下特点:

  • UI 无关 (在 Presenter 中不能包含 UIKit 相关头文件);
  • 处理展示逻辑;
  • Model 与 View 间的桥接者。

Model View View-Model


最近两三年对 MVVM(Model View View-Model) 的讨论比较多,其提出的愿景也是为了简化 Controller、彻底将 View 与 Model 解耦、并提供 Data Binding。

在 MVVM 中 Controller 被认为是 View,更准确的说是:

Rules


在 MVVM 模式中,各模块间的依赖关系、数据流向、数据传递的格式都有严格的规定:

如上图所示,View、View Model 以及 Model 需要遵守以下规则:

  • View(UIViewController/UIView):
    1.可以依赖(持有) View、View Model,即可直接调用其方法;
    2.不能依赖(持有) Model、Model Object(Item);
    3.UI 绑定到View Model上(如:titleLabel.text->viewModel.title)
    4.通过 RACCommands/RACActions 或直接调用 View Model 的方法将用户事件传递给 View Model。

  • View Model:
    1.可以依赖(持有) View Model、Model 以及 Model Object;
    2.不能依赖(持有) View、Raw Model Object;
    3.其公开属性只能是基础数据类型(NSInteger、NSString等)或其他 View Model;
    4.将 Model Object 转换成可直接在 View 上显示的属性或Sub View Model(展示逻辑);
    5.接受来自 View 或 Sub View Model 的输入(用户事件)。

  • Model (Layer):
    1.可以依赖(持有) 其他 Model、Model Object、Data Source、Raw Model Object;
    2.不能依赖(持有) View Model、View;
    3.将 Raw Model Object 转换为 Model Object;
    4.为 View Model 提供数据(异步)。

其中,View 与 View Model 类似 UIView 与 CALayer 的关系,一一对应(包括层次结构):

Data Binding



从上图可知,在 MVVM 中数据流方向与依赖关系正好相反,数据流的流动就是建立在 Observer Synchronization 思想基础之上。

从 Data Source 到 Model、Model 到 View Model 可采用一般的同步方法,如:Delegate、Notification 以及 block 等。而从 ViewModel 到 View 的 Data Binding 是 MVVM 模式与其他 MV* 模式最大的区别。

遗憾的是 iOS 并没有原生的 Data Binding 方式,目前大概只能通过两种方式实现 Data Binding:KVO 或 ReactiveCocoa。KVO/RAC是一种更加激进的 Observer Synchronization:

  • 优势:绑定关系确定后,同步更加方便;
  • 劣势:数据流不直观,调试较困难;

MVVM VS. MVP


MVVM 与 MVP 有很多相似的点:

  • 将展示逻辑从 Controller 中提取出来(分别放到 View Model 和 Presenter 中);
  • 分别在 View Model、Presenter 中响应用户事件;
  • 分别通过 View Model、Presenter 连接 View 与 Model;
  • 解耦 View 与 Model。

两者最大的区别在于:MVVM 有 Data Binding 而 MVP 没有。

华山论剑——MV* VS. MVVM


根据是否有 Data Binding,可将常见 GUI 构架分为两大阵营:

  • 有 Data Binding:MVVM;
  • 没有 Data Binding:MVC、MVP、MVCS 等。

MVVM 的 Data Binding 在一定程度上增加了编码的复杂度、数据流也变得不够直观、调试难度也有所增加。但对于数据可变的场景,一旦通过 Data Binding 将 View 与 View Model 绑定起来,在数据变化时,会自动映射到 UI 上,十分方便。

根据展示逻辑是否独立于 Controller,可分为:

  • 独立:MVVM、MVP
  • 不独立:MVC、MVCS

MVVM、MVP 分别将展示逻辑从 Controller 中提取出来,使 Controller 得到一定程度的简化,在展示逻辑复杂的情况下,效果更加明显。

没有好坏,只有适合


通过上述分析,我们可以看到,常见几种架构:MVC、MVCS、MVP、MVVM 并没有绝对的好坏之分,只是各有不同的适用场景。
我们在选择时可以根据以下两点作为参考依据:

  • 数据是否可变(UI是动态还是静态)
    静态:MV*
    动态:MVVM
  • 展示逻辑是否复杂
    复杂:MVVM、MVP

万变不离其宗——MVC是根


MVCS、MVP、MVVM 等各种新生架构,虽各有不同,但都是源自于 MVC,它们的核心思想一直没变,也不能变:

  • Separated Presentation;
  • Observer Synchronization.

实例


理论的东西讲了不少,下面结合实际的项目,看看应该如何选择架构。

书籍详情页


书籍详情页在整个 QQ 阅读 app 中,无论是展示还是业务逻辑都是最复杂的一个模块。

  • 在书籍下载过程中下载按钮需要显示下载状态(进度)——动态 UI;
  • 评分、作者分类、包月相关提示、打赏、粉丝榜等展示逻辑十分复杂。

因此,该模块采用 MVVM 架构比较合适。遗憾的是,当时设计该模块时没有充分意识到其复杂程序,而是选择了传统的 MVC 架构。结果造成详情页 Controller 十分复杂,下面这段就是 Controller 中根据下载状态修改 toolbar 上3个按钮状态的代码(ps:看不清没关系,只要能看出其很复杂即可^_^):

同时,大量的展示逻辑也耦合在了各个 View 中:

如果,采用 MVVM 架构,各种展示逻辑可以放到相应的 View Model 中,让 View 只专注于 layout。同时在 View Model 中处理下载相关逻辑,使下载逻辑与 View 解耦。

信息流


信息流作为 QQ 阅读一大亮点,能为用户个性化推荐书籍,是整个 app 中最重要的一个页面:

信息流是最重要的页面,但不是最复杂的页面:

  • 信息流的数据是静态的,在显示过程中不会改变——静态UI;
  • 展示逻辑相对简单。

因此,信息流模块没必要使用复杂的 MVVM 架构。
很多场景属于此类情形:通过 UITableView 列举多行静态数据,若数据有更新时直接 reload tableview。

无剑胜有剑 皆可为剑


各种分层架构都是前辈充满智慧的宝贵经验,值得尊敬、借鉴、学习,但也不必拘泥于形式,重点是理解其背后的思想。设计模式有六大原则:

  • 单一职责原则
  • 里氏代换原则
  • 依赖倒转原则
  • 接口隔离原则
  • 迪米特法则
  • 开放-封闭原则

其中除了里氏代换原则,其他五大原则都是分层架构的指导思想。只要我们深刻理解并能严格遵守这些原则,无论我们选择哪种架构、或在其基础上进行衍化,都能设计出高质量的代码。

小结


代码设计、架构选择及理解仁者见仁、智者见智,但经典的设计理念是公认的、也是经过时间检验的。

参考资料

GUI Architectures
Model-View-Controller
Introduction to MVVM
Lighter View Controllers
ReactiveCocoa and MVVM, an Introduction
On MVVM, and Architecture Questions