首页

时间和精神的房子
壹只iOS程序员的修行世界,欢迎来访

如果文章对您有所帮助
将是我最大的荣幸

iOS 中 MVC 的设计理念

MVC 的概念最早出现在二十世纪八十年代的 施乐帕克实验室 中,当时施乐帕克为 Smalltalk 发明了这种程序设计模式。现在,MVC 已经成为主流的客户端编程框架,在 iOS 开发中,系统为我们实现好了公共的视图类:UIView 和控制器类 UIViewController。因此,我们几乎无法逃避开 MVC 这种设计模式。

理想MVC

MVC 将程序中的类分为三种类型Model、View、Controller,以便每种类型的类可以相互解耦提高灵活性和复用性。

Model 用于封装与应用程序的业务逻辑相关的数据以及对数据的处理方法。狭义来讲,Model 是对数据的封装,较现代的 Framework 都会建议使用独立的数据对象来替代弱类型的集合对象。而广义上讲,Model 不仅仅是对数据的封装,而是承载了明确的业务逻辑的处理(Service 层)。当然这些业务逻辑在普通程序中可能只是简单的网络请求(Network Service)和数据库操作(Database Service)。

Model 不依赖 View 和 Controller,也就是说, Model 不关心它会被如何显示或是如何被操作。但是 Model 中数据的变化一般会通过一种刷新机制被公布。

View 能够实现数据有目的的显示。在 View 中一般没有程序上的逻辑。为了实现 View 上的刷新功能,View 需要访问它监视的数据 Model。

Controller 起到不同层面间的组织作用,用于控制应用程序的流程。它处理事件并作出响应。“事件”包括用户的行为和数据 Model 上的改变。

在理想的 MVC 中,上述理念得到最广泛的认可,同时这也是 MVC 成为一个解决方案的关键,即视图和逻辑分离。但 MVC 真正的实现中却很难做到这样。MVC 的理念在不同的 Framework 中都有不同的实现。这些实现有好有坏,MFC 中 View/Document 的实现就属于失败的 MVC 作品。

举例

假设现在有一个算法类程序,比如围棋。Model 层就负责实现棋子类、棋盘类等数据建模和AI算法类对数据进行处理。对于这类算法程序,Model 层就是程序的核心,我们甚至不需要 View 而让现实世界来充当。我们可以直接定义一个简单的 Controller 以接受用户从命令行输入的落棋坐标,坐标信息由 AI 类根据棋盘模型中的棋子进行计算后返回 AI 期望的落棋坐标,然后由人类玩家在现实世界的 View(棋盘)中来下棋。

假设程序用 C++ 编写,我们 Model 层的代码就可以在没有任何改动的情况下在 iOS、Android、MacOS、Linux、Windows 等多个平台上编译执行。如果程序在不同平台均用控制台操控,那么 Controller 也可以进行复用。我们要做的仅仅是根据不同平台的 Framework 实现对应的 View(Windows 的 QT,iOS 的 CocoaTouch 等),监控 Model 层(棋盘类)的改变并将改变在 View 中展现出来。

当然这只是假设,我们不可能在不同平台都用控制台来操纵程序。所以我们还是应该实现不同平台的 Controller 来接受用户用鼠标、键盘或者触摸等方式的输入。然而每个平台实现对事件的处理也不尽相同,所以 Controller 也要依赖不同平台的 Framework 实现。

移动端 MVC

轻量的 Model

当我们把根据程序输出在在现实世界下棋的玩家换成 http 连接时,是不是觉得这个程序就很接近现在客户端软件的架构了。客户端来在探讨的 MVC 对于服务端来说只有 View 和 Controller,复杂的业务逻辑和数据处理都放在服务端了。所以客户端的 Model 轻到只需要管理从服务端获取的数据。所以对于简单的客户端应用,Model 层的数据封装只用将服务端返回的 Json 转成对应的数据对象,然后加上网络和数据库的 Service 来获取和缓存这些数据对象实现对数据的管理。

而客户端对 Model 层这些简单的需求都已经被系统的开发框架(CocoaTouch)或第三方实现了。所以才能看到现在 iOS 开发中无脑的导入 AFNetworing(数据获取)、FMDB(数据缓存)、Mantle(数据对象建模) 等知名第三方后,就算完成了 Model 层的搭建。

MVCS 将 Model 的数据封装和数据处理分开,Model 只负责将数据封装成强类型的 Model,Service 负责数据的存取和管理。

Controller 为什么强制绑定 View

也许你早已经发现,iOS 中所有 App 界面交互的逻辑都基本被 UINavigationController、UITablbarController 和 UITableViewController 所定义。所以 CocoaTouch 就是根据苹果对智能手机基本交互方式的构想而设计的。这的确定义并影响了整个智能手机的交互模式。开发框架的职责就是要方便开发者的开发,为基本的交互逻辑提供默认实现。对于 CocoaTouch 而言,带触摸滑动效果的页面交互,带导航栏的页面间切换、带标签栏的的业务模块间切换和屏幕旋转等交互,都是苹果定义智能手机的核心交互方式。

对于交互逻辑自然是由 Controller 来定义。所以你看到了现在的 UINavigationController 等各种 Controller 容器的默认实现。然而这些交互逻辑都是针对 View 的,如果 Controller 里没有默认的 View 怎么来实现 View 的旋转、切换等默认交互。所以 CocoaTouch 才会将 Controller 绑定了一个空的容器 View。你可以通过 loadView 方法来自己生成这个 View,也可以让 Controller 用懒加载的方式自动帮你生成,然后在 viewDidLoad 时来真正定义这个容器 View 的样式(Subview)。

用户事件为什么由 View 传递

在理想的 MVC 模型中,应该由 Controller 处理事件。而 iOS 中 CocoaTouch 为我们封装了可以处理特定事件的 View。当我们使用 UITableView 时,只需要滑动屏幕,TableView 就会跟着我们的手势滑动,我们并没有在 Controller 中处理这些类似事件。事实上大部分 UI 开发架构都通过 View 来传递并处理事件,对于那些与业务相关的事件才分发给 Controller 去处理。

如果我们为每种 View 都定义特定的 Controller 来实现这些默认行,这绝对不是个好的 MVC 实现。并不是每个 View 都需要在 MVC 中找到归属。大部分情况下,这些 View 只是 MCV 中 View 的组成,它们不需要有自己对应 Model 和 Controller。

所以,为了有利于 View 的封装和复用,让 View 来传递并处理与业务无关的事件是很好的方案。这大大减轻了开发者的负担。

View 为什么不监听 Model

iOS 中的软件开发已经变得十分复杂,Model 的改变与 View 的刷新并不是简单的对应关系。具体来说,某个 View 的显示可能被多个 Model 共同影响,并且这种影响有时还会被很多变量共同决定。这导致 View 直接监听 Model 变得不可实现。如果 View 去监听多个 Model 和这些变量,那么不仅会使软件变得无比复杂,还会使这些本应该由 Controller 负责页面显示规则代码耦合在 View 中,使得 View 无法复用。所有应该让 Controller 来处理数据改变的回调,然后对这些回调返回的数据进行处理和判断后再去操作对应 View 的刷新。

理想 MVC 架构中让 Model 监听 View 的设计只适合拥有简单逻辑的页面,比如 GOF 书中 MVC 的例子——图表应用。所以 iOS 中的这种改进是如此的必要,以至于很多博客在讲述 MVC 时直接就将这种改进当做理想的 MVC 来讲述。这对很多初学者造成了很大的困惑。

我们为什么要纠结 iOS 中的 MVC

之所以把 Smalltask 定义的 MVC 称为理想 MVC,是因为在理想的情况这种设计模式的确很优秀,但事实上软件开发太多样化,它不能完全适合所有的平台的软件。这也正是每个平台提供的开发框架对 MVC 的实现都不尽相同的原因。为什么它是一种伟大的设计模式,是因为它的理念很好的解决了软件开发的痛点,提高了复用性。我们只需要在意他的核心理念,即 UI 和逻辑的分离。明白了这点,只需要按照每个平台开发者文档告诉我们的那样开发,就是在正确的在使用 MVC。何必要纠结为什么这与别的平台的 MVC 有什么不同。

为什么 iOS 中 Controller 容易膨胀

在 iOS 中的 MCV 如果使用不当,很可能让大量代码都集中在 Controller 中,让 MVC 变成 Massive View Controller。

很多 Controller 的膨胀是因为业务逻辑的堆积。这种情况下大多是因为没有正确的使用 MVC。Controller 中只应该处理必要的事件,再根据事件去修改 Model,然后通过修改成功的回调再去更新对应的 View。业务逻辑显然不应再堆积在 Controller 中,复杂的数据处理逻辑应该属于 Model 层。具体的做法是,不要在 Controller 中直接使用 AFNetworing 或 FMDB 等数据存取和处理的 API,而应该将这些 API 用具体的业务逻辑封装成 Service。这样只要传入必要的参数到 Service,经过 Service 处理后再去调用这些 API 存取或修改数据,等操作结果返回后,同样先由 Service 处理结果再返回给 Controller,这样数据处理的逻辑放在了 Service 中。这种模式也被称为 MVCS(Service)。MVCS 将 Model 层的功能剥离到只剩下 Model(数据封装)。

另一种情况是因为 Controller 要处理的事件太多。比如地图软件的主页、国产浏览器的主页等集中了软件大部分核心功能的 Controller。这种 Massive Controller 一般拥有大量的事件回调、View 的切换效果和逻辑复杂、带有复杂的动画等。这种情况在 iOS 的 MVC 中很难避免,但至少属于小众情况。不过我们仍然可以通过重构来分散这些功能,这里不再深入探讨。你可以通过 唐巧 的这篇文章了解怎么重构分散这些功能。

参考资料

[Wiki] MVC
MVVM 介绍
MVC,MVP 和 MVVM 的图示
[概念理解] MVC模式和C++的实现
iOS应用架构谈 view层的组织和调用方案
iOS 架构模式 - 简述 MVC, MVP, MVVM 和 VIPER (译)
[CrespoXiao] iOS大型项目开发漫谈
被误解的MVC和被神化的MVVM

关注作者

分享本文

目录