每一个新的iOS开发者都必须马上熟练掌握一大堆东西:新的语言,新的框架以及Apple推崇的设计模式:模型 - 视图 - 控制器(MVC模式)
尽快上手iOS开发总是个令人畏缩的事,而且更多的情况是开发者并没有没有花很多功夫在理解MVC模式上,而这也更让开发者在歧途上越走越远。
这篇文章能帮你避免app缺乏扩展性的问题,同时你将会学到如何正确使用MVC模式来构造你的app。这些最好实践建议都是来自于原来在学校不断地练习和失败。
当看完这篇文章,相信你就会明白怎样在你的app中实践这种最优化的设计模式同时最大程度上避免出现让你头痛的结构性问题。let's go!
MVC 101
提醒:如果你了解MVC的概念,可以直接跳过这一节到下一节来直接开始看如何实践。
从一个更高的层次来看,MVC正如它名字所示的一样由三部分组成:模型,视图和控制器。
模型是你储存数据的地方。很多东西像类,映射关系,网络层的代码等等都本放在这里。
视图层是你app的外观。它基本上是可重复使用的,因为这里并没有储存具体实现层面的内容。例如像
UILabel
就是一个在屏幕上展示文字的视图,可以很容易复用在其余地方。控制器处在模型和视图之间,一般都是靠代理模式来实现的。在理想的情况下,控制器不知道它所使用的视图的具体实现。相反的,它通过接口协议(protocol)来与视图进行交流。一个经典的例子是
UITableView
和它的数据源Data Source使用UITableViewDataSource
协议来进行交流。
当你把所有的东西放在一块,它看起来像这样:
很简单吧?
正如他们所说,魔鬼在细节里面。当你真正开始实践MVC时,事情就会变得非常棘手,你马上会看到的。
苹果的官方MVC文档解释了这些细节,可以给你一个非常理论的认识,能帮你避免一些可能的陷阱。
但是从实际的角度来看,这需要了解太多无关的东西。所以我们还是少谈理论直接上手吧!
从哪儿开始?
虽然理解MVC的理论很容易,但有时实践最初的时候很难一下搞懂从哪儿开始下手。我们花点时间来看看吧。
视图层 View Layer
当用户和你的app开始交互,他们实际是与视图层在进行交互。视图一般被当做你app中的“傻帽”,因为它并没有包含任何具体工作逻辑。从代码的角度,你将会在这一层看到这些东西:
UIView
的子类。他们来自最基础的UIView
并可以通过自己的定制来不断复杂。一个
UIViewController
(有争议)。因为UIViewController和它自己的根UIView紧密联系在一起和它自己的生命周期(lifecycle),我个人认为它属于视图层,但不是每个人都同意。动画和
UIViewController
的过渡UIKit/Appkit, Core Animation 和 Core Graphics中的类
典型的代码异味(Smell Code)在这一层有很大的栖息空间,那些与UI无关的代码将很快让你的程序出现问题。一个典型的代码异味就是从UIViewController
发起一个网络请求。
这些习惯会让你放入很多代码在UIViewController
中,这也让你离死期不远了。不要这样做!短期来说,你节约了几分钟,但长期来看,你可能会损失几个小时来debug或者在其他地方没办法复用这些代码。
当你检查视图层的时候参看一下以下的问题:
它和模型层(model layer)有交互吗?
它包含工作逻辑吗?
它会做与UI无关的事吗?
如果有任意一个问题你的答案是”是“,或许你该花点时间来清理和重构一下你的代码了。
当然,这些规则不是一成不变的,有时你还是会需要选择忽视它们。不管怎么说,在开发过程中需要时刻注意这些准则。
最后,如果你把这些类写得很好,你应该能随时复用他们。如果你不相信我,你只需要看看GitHub上面有多少UI元素。
控制层 Controller Layer
控制器层是你app中复用度最低的部分,因为它包含着你很多具体的操作逻辑。在你这里成立的代码不能在别人那里使用,这应该不会让你觉得意外。
通常来说,你会在控制层看到很多类来处理以下事情:
什么需要被首先访问:数据还是网络?
多久刷新一次你的app?
下一个界面应该是什么?在什么样的情景下?
如果app被推到后台,有什么需要被清理?
...
你应该把控制层看做app的大脑:它负责下一步怎么走。通常的情况下,你需要做大量的测试来保证控制层中的类都如预期一样工作。
一个栗子
你现在应该对控制层有个更好的理解了,让我们来看看他在实际情况下如何运行的。
提醒:如果你想看看具体的app情境,你可以下载这个我做的简单示例app。
设想你有一个UIViewController的子类想知道今年WWDC出席者的名单。为了达到这个目标,你需要利用Controller类。因为苹果已经告诉我们需要使用协议,你必须做到:
enum UIState {
case Loading
case Success([Attendee])
case Failure(Error)
}
protocol WWDCAttendesDelegate: class {
var state: UIState { get set}
}
你的想法是先设置state来Loading,然后当WWDC的出席者名单被载入(或者失败)时更新UI。
因为你不想UIViewController
来处理这个响应,它会使用几个实现了WWDCAttendesDelegate协议的类(WWDCAttendeesUIViewController
)来处理。这种隔离可以让你独立地测试WWDCAttendeesUIViewController
。
下一步是创建一个抽象协议给这个Controller,这样你就可以将它添加到你的UIViewController:
protocol WWDCAttendeesHandler: class {
var delegate: WWDCAttendesDelegate? { get set }
func fetchAttendees()
}
从UIViewController子类的角度来看的话,具体的实现应该类似于以下的代码:
init(attendeesHandler: WWDCAttendeesHandler) {
self.attendeesHandler = attendeesHandler
super.init(nibName: nil, bundle: nil)
}
override func viewDidLoad() {
super.viewDidLoad()
atteendeesUIController = WWDCAttendeesUIController(view: view, tableView: tableView)
attendeesHandler.delegate = atteendeesUIController
attendeesHandler.fetchAttendees()
}
这种实现将会将取回数据的动作放到UIViewController中,但是应答的部分放在WWDCAttendeesUIViewController:
extension WWDCAttendeesUIController: WWDCAttendesDelegate {
func update(newState: UIState) {
switch(state, newState) {
case (.Loading, .Loading): loadingToLoading()
case (.Loading, .Success(let attendees)): loadingToSuccess(attendees)
default: fatalError("Not yet implemented \(state) to \(newState)")
}
}
func loadingToLoading() {
view.addSubview(loadingView)
loadingView.frame = CGRect(origin: .zero, size: view.frame.size)
}
func loadingToSuccess(attendees: [Attendee]) {
loadingView.removeFromSuperview()
tableViewDataSource.dataSource = attendees.map(AttendeeCellController.init)
}
}
你可以把WWDCAttendeesUIViewController当做UI的大脑,而同时也作为事务操作的核心。
哈,也没那么难嘛!但是这个例子提出了一个问题:谁创建了Controller?
我一直推荐把Controller保持可插入性,这样你UIViewController的所有者就可以提供这个controller。这有两个最主要的好处:
非常良好的可测试性。你可以通过任何实现了FetchNumberOfTickets协议的对象。
层级间被干净地分开了。这帮助你确定每部分的责任,让你的代码更加健康。
模型层 Model Layer
模型层不是它自己所解释的那样。
正如你所期待的一样,它拥有你自己定义的模型对象,出现在所有层级中。在门票的例子中,你将会把Ticke
struct 放在你的模型中。
我认为这些部分将会出现在模型层中:
网络 Network Code。外观应该长得像这样。理想情况下,在你整个APP中,应该只需要一个类来负责网络部分。
储存 Persistence Code。你可以用Core Data或者直接保存为NSData在磁盘上。
解析 Parsing Code。任意解析网络应答的对象都应该被包含在模型层中。
一般来说模型的对象和解释器都是和具体情景相关的,但网络部分的代码都是高度可复用的。
控制器会使用到你在模型层里定义的所有元素来组织你app中的所有信息流。
MVC:Massive View Controller?
对于那些还不知道的同学,当一个UIViewController开始要求不属于它的责任时,它被我们叫做Massive View Controller。这一般从在控制器中加入网络部分,解析部分开始,之后代码不断变得越来越臃肿,直到你完全失去对信息流的控制。更要命的是,你没有办法安全地进行重构,因为你根本没有办法进行单元测试。
对于”巨型View Controller“来说并没有一个很好的解决办法,所以它一般被放在一旁作为技术负债(technical debt)谁都不想处理。这是iOS开发社区中一个常见的问题,这也是为什么MVC名声不咋地。
但是Massive View Controller并不一定是你的终点!
作为头等重要的守则,一个UIViewController不能超过130行代码。这可能一下很难达到,但如果你足够自律,还是不难的。这里有些准则我一般遵从的:
大部分ViewController中的代码都和UIView的定制性有关
控制器应该负责将控制器和UIView关连起来。例如:通过
IBAction
访问控制器的方法代理协议及其实现不应该包含在控制器。如果在的话就很难进行测试了。
如果你的ViewController有太多属性,你可以考虑将其分解成几个小的ViewController或者直接创建一个新的UIView
这些只是一些指导意见,有时你的UIViewController非常简单并不需要来满足这些准则。永远记得当你给你的控制器分配新的责任,你就放弃了一点测试和复用这些代码的机会。
下一步怎么走?
MVC比我们很多人都出现得早,同时它还会继续存在着。虽然它最近的名声不怎么样,它仍然不应该给我们在app中犯的错误背锅。
MVC更多的是像一个建造app的蓝图,同时预留了很多空间给我们来自己决定。你应该把它当做一种指导准则,像一个菜谱。不管怎么说,它都留了一堆事情让你自己来决定。这有好有坏。它很好因为它给了你设计灵活性,他很坏也因为你缺乏足够的经验而经常做出错误的决定。
没有什么架构是一招制敌的,不管是新还是旧,而你首先应该做到的是关注优秀的工程原则。
如果我能年轻的我一些关于MVC的建议话,我会说:
首先,理解每个对象的责任。一旦你理解了这,就开始考虑代码。
不要低估了模块的独立性。这能够给你的代码带来奇迹,不管是复用性上还是可测试性上。
尽力让UIViewController远离操作逻辑。控制器越干净,它的的行为就更合理。
祝你能和MVC搞好关系 - 如果你能满足这些准则,你就能把MVC当成你的朋友而不是敌人。
翻译自:Model-View-Controller (MVC) in iOS: A Modern Approach
Rui Peres:Rui has been developing for iOS since 2010, when he started in Portugal. Nowadays, he is a Lead Software Engineer at MailOnline based in London. When he is not a paperboy, you can either find him on Stack Overflow, Github or at codeplease.io. On Thursday you will for sure notice him when you get his awesome newsletter (iOS Goodies). Besides coding, he loves going to the gym (do you even lift bro?) and playing League of Legends.