为什么要关注架构设计?
因为假如你不关心架构, 那么总有一天, 需要在同一个庞大的类中调试若干复杂的事情, 你会发现在这样的条件下, 根本不可能在这个类中快速的找到以及有效的修改任何 bug. 当然, 把这样的一个类想象为一个整体是困难的, 因此, 有可能一些重要的细节总会在这个过程中会被忽略
架构模式
一个整体的分层? 逻辑清晰? 还是清晰的分工? 对于架构模式来说并没有一个非常明确的定义, 比较抽象, 在于设计在于架构, 不管是小到类与类之间的交互, 还是不同的小模块, 小版块之间, 甚至于在不同的业务之间, 我们都可以去从架构的方面去理解, 去分析
那么在 iOS 中常见的架构来说, 常见有就有这些: MVC 架构, MVP 架构, MVVM 架构
当然这些架构都有一个共同特点: 解耦合
MVC
什么是 MVC?
MVC 最早存在于桌面程序中的, M 是指业务数据, V 是指用户界面, C 则是控制器. 在具体的业务场景中, C 作为 M 和 V 之间的连接, 负责获取输入的业务数据, 然后将处理后的数据输出到界面上做相应展示, 另外, 在数据有所更新时, C 还需要及时提交相应更新到界面展示. 在上述过程中, 因为 M 和 V 之间是完全隔离的, 所以在业务场景切换时, 通常只需要替换相应的 C, 复用已有的 M 和 V 便可快速搭建新的业务场景. MVC 因其复用性, 大大提高了开发效率, 现已被广泛应用在各端开发中
MVC 的过去
在我们探讨 Apple 的 MVC 模式之前, 我们来看下传统的 MVC 模式
在图里, View 并没有任何界限, 仅仅是简单的在 Controller 中呈现出 Model 的变化想象一下, 就像网页一样, 在点击了跳转到某个其他页面的连接之后就会完全的重新加载页面尽管在 iOS 平台上实现这这种 MVC 模式是没有任何难度的, 但是它并不会为我们解决架构问题带来任何裨益因为它本身也是, 三个实体间相互都有通信, 而且是紧密耦合的这很显然会大大降低了三者的复用性, 而这正是我们不愿意看到的
然而传统的 MVC 架构不适用于现在的 iOS 开发
苹果推荐的 MVC--- 愿景
由于 Controller 是一个介于 View 和 Model 之间的协调器, 所以 View 和 Model 之间没有任何直接的联系 Controller 是一个最小可重用单元, 这对我们来说是一个好消息, 因为我们总要找一个地方来写逻辑复杂度较高的代码, 而这些代码又不适合放在 Model 中理论上来讲, 这种模式看起来非常直观, 但你有没有感到哪里有一丝诡异? 你甚至听说过, 有人将 MVC 的缩写展开成(Massive View Controller), 更有甚者, 为 View Controller 减负也成为 iOS 开发者面临的一个重要话题如果苹果继承并且对 MVC 模式有一些进展, 所有这些为什么还会发生?
苹果推荐的 MVC-- 事实
Cocoa 的 MVC 模式驱使人们写出臃肿的视图控制器, 因为它们经常被混杂到 View 的生命周期中, 因此很难说 View 和 ViewController 是分离的尽管仍可以将业务逻辑和数据转换到 Model, 但是大多数情况下当需要为 View 减负的时候我们却无能为力了, View 的最大的任务就是向 Controller 传递用户动作事件 ViewController 最终会承担一切代理和数据源的职责, 还负责一些分发和取消网络请求以及一些其他的任务
你可能会看见过很多次这样的代码:
- varuserCell = tableView.dequeueReusableCellWithIdentifier("identifier") as UserCell
- userCell.configureWithUser(user)
这个 cell, 正是由 View 直接来调用 Model, 所以事实上 MVC 的原则已经违背了, 但是这种情况是一直发生的甚至于人们不觉得这里有哪些不对如果严格遵守 MVC 的话, 你会把对 cell 的设置放在 Controller 中, 不向 View 传递一个 Model 对象, 这样就会大大增加 Controller 的体积
直到进行单元测试的时候才会发现问题越来越明显因为你的 ViewController 和 View 是紧密耦合的, 对它们进行测试就显得很艰难 -- 你得有足够的创造性来模拟 View 和它们的生命周期, 在以这样的方式来写 View Controller 的同时, 业务逻辑的代码也逐渐被分散到 View 的布局代码中去
我们看下一些简单的例子:
- import UIKit
- struct Person { // Model
- let firstName: String
- let lastName: String
- }
- class GreetingViewController : UIViewController { // View + Controller
- varperson: Person!
- let showGreetingButton = UIButton()
- let greetingLabel = UILabel()
- override func viewDidLoad() {
- super.viewDidLoad()
- self.showGreetingButton.addTarget(self, action: "didTapButton:", forControlEvents: .TouchUpInside)
- }
- func didTapButton(button: UIButton) {
- let greeting = "Hello"+ ""+ self.person.firstName +" "+ self.person.lastName
- self.greetingLabel.text = greeting
- }
- // layout code goes here
- }
- // Assembling of MVC
- let model = Person(firstName: "David", lastName: "Blaine")
- let view = GreetingViewController()
- view.person = model;
这段代码看起来可测试性并不强, 我们可以把和 greeting 相关的都放到 GreetingModel 中然后分开测试, 但是这样我们就无法通过直接调用在 GreetingViewController 中的 UIView 的方法 (viewDidLoad 和 didTapButton 方法) 来测试页面的展示逻辑了, 因为一旦调用则会使整个页面都变化, 这对单元测试来讲并不是什么好消息事实上, 在单独一个模拟器中 (比如 iPhone 4S) 加载并测试 UIView 并不能保证在其他设备中也能正常工作, 因此我建议在单元测试的 Target 的设置下移除 "Host Application" 项, 并且不要在模拟器中测试你的应用
以上所述, 似乎 Cocoa MVC 看起来是一个相当差的架构方案我们来评估一下 MVC 一系列的特征:
任务均摊 --View 和 Model 确实是分开的, 但是 View 和 Controller 却是紧密耦合的
可测试性 -- 由于糟糕的分散性, 只能对 Model 进行测试
易用性 -- 与其他几种模式相比最小的代码量熟悉的人很多, 因而即使对于经验不那么丰富的开发者来讲维护起来也较为容易
如果你不想在架构选择上投入更多精力, 那么 Cocoa MVC 无疑是最好的方案, 而且你会发现一些其他维护成本较高的模式对于你所开发的小的应用是一个致命的打击
MVP
MVC 的缺点在于并没有区分业务逻辑和业务展示, 这对单元测试很不友好. MVP 针对以上缺点做了优化, 它将业务逻辑和业务展示也做了一层隔离, 对应的就变成了 MVCP. M 和 V 功能不变, 原来的 C 现在只负责布局, 而所有的逻辑全都转移到了 P 层
MVP 实现了 Cocoa 的 MVC 的愿景
这看起来不正是苹果所提出的 MVC 方案吗? 确实是的, 这种模式的名字叫做 MVC, 但是, 这就是说苹果的 MVC 实际上就是 MVP 了? 不, 并不是这样的如果你仔细回忆一下, View 是和 Controller 紧密耦合的, 但是 MVP 的协调器 Presenter 并没有对 ViewController 的生命周期做任何改变, 因此 View 可以很容易的被模拟出来在 Presenter 中根本没有和布局有关的代码, 但是它却负责更新 View 的数据和状态就 MVP 而言, UIViewController 的子类实际上就是 Views 并不是 Presenters 这点区别使得这种模式的可测试性得到了极大的提高, 付出的代价是开发速度的一些降低, 因为必须要做一些手动的数据和事件绑定, 从下例中可以看出:
- import UIKit
- struct Person { // Model
- let firstName: String
- let lastName: String
- }
- protocol GreetingView: class {
- func setGreeting(greeting: String)
- }
- protocol GreetingViewPresenter {
- init(view: GreetingView, person: Person)
- func showGreeting()
- }
- class GreetingPresenter : GreetingViewPresenter {
- unowned let view: GreetingView
- let person: Person
- required init(view: GreetingView, person: Person) {
- self.view = view
- self.person = person
- }
- func showGreeting() {
- let greeting = "Hello"+ ""+ self.person.firstName +" "+ self.person.lastName
- self.view.setGreeting(greeting)
- }
- }
- class GreetingViewController : UIViewController, GreetingView {
- varpresenter: GreetingViewPresenter!
- let showGreetingButton = UIButton()
- let greetingLabel = UILabel()
- override func viewDidLoad() {
- super.viewDidLoad()
- self.showGreetingButton.addTarget(self, action: "didTapButton:", forControlEvents: .TouchUpInside)
- }
- func didTapButton(button: UIButton) {
- self.presenter.showGreeting()
- }
- func setGreeting(greeting: String) {
- self.greetingLabel.text = greeting
- }
- // layout code goes here
- }
- // Assembling of MVP
- let model = Person(firstName: "David", lastName: "Blaine")
- let view = GreetingViewController()
- let presenter = GreetingPresenter(view: view, person: model)
- view.presenter = presenter
MVP 是第一个如何协调整合三个实际上分离的层次的架构模式, 既然我们不希望 View 涉及到 Model, 那么在显示的 View Controller(其实就是 View)中处理这种协调的逻辑就是不正确的, 因此我们需要在其他地方来做这些事情例如, 我们可以做基于整个 App 范围内的路由服务, 由它来负责执行协调任务, 以及 View 到 View 的展示这个出现并且必须处理的问题不仅仅是在 MVP 模式中, 同时也存在于以下集中方案中
我们来评估一下 MVP 一系列的特征:
任务均摊 -- 我们将最主要的任务划分到 Presenter 和 Model, 而 View 的功能较少(虽然上述例子中 Model 的任务也并不多)
可测试性 -- 非常好, 由于一个功能简单的 View 层, 所以测试大多数业务逻辑也变得简单
易用性 -- 在我们上边不切实际的简单的例子中, 代码量是 MVC 模式的 2 倍, 但同时 MVP 的概念却非常清晰
MVP-- 绑定和信号
还有一些其他形态的 MVP-- 监控控制器的 MVP
这个变体包含了 View 和 Model 之间的直接绑定, 但是 Presenter 仍然来管理来自 View 的动作事件, 同时也能胜任对 View 的更新
但是我们之前就了解到, 模糊的职责划分是非常糟糕的, 更何况将 View 和 Model 紧密的联系起来这和 Cocoa 的桌面开发的原理有些相似
MVVM
MVVM 架构是 MV(X)系列最新的一员, 因此让我们希望它已经考虑到 MV(X)系列中之前已经出现的问题从理论层面来讲 MVVM 看起来不错, 我们已经非常熟悉 View 和 Model, 以及 Meditor, 在 MVVM 中它是 View Model
它和 MVP 模式看起来非常像:
MVVM 将 ViewController 视作 View
在 View 和 Model 之间没有紧密的联系
此外, 它还有像监管版本的 MVP 那样的绑定功能, 但这个绑定不是在 View 和 Model 之间而是在 View 和 ViewModel 之间
在 iOS 中 ViewModel 实际上代表什么? 它基本上就是 UIKit 下的每个控件以及控件的状态 ViewModel 调用会改变 Model 同时会将 Model 的改变更新到自身并且因为我们绑定了 View 和 ViewModel, 第一步就是相应的更新状态
绑定
我在 MVP 部分已经提到这点了, 但是该部分我们仍会继续讨论
如果我们自己不想自己实现, 那么我们有两种选择:
基于 KVO 的绑定库如 RZDataBinding 和 SwiftBond
完全的函数响应式编程, 比如像 ReactiveCocoaRxSwift 或者 PromiseKit
事实上, 尤其是最近, 你听到 MVVM 就会想到 ReactiveCoca, 反之亦然尽管通过简单的绑定来使用 MVVM 是可实现的, 但是 ReactiveCocoa 却能更好的发挥 MVVM 的特点
但是关于这个框架有一个不得不说的事实: 强大的能力来自于巨大的责任当你开始使用 Reactive 的时候有很大的可能就会把事情搞砸换句话来说就是, 如果发现了一些错误, 调试出这个 bug 可能会花费大量的时间, 看下函数调用栈:
在我们简单的例子中, FRF 框架和 KVO 被过渡禁用, 取而代之地我们直接去调用 showGreeting 方法更新 ViewModel, 以及通过 greetingDidChange 回调函数使用属性
- import UIKit
- struct Person { // Model
- let firstName: String
- let lastName: String
- }
- protocol GreetingViewModelProtocol: class {
- vargreeting: String? { get }
- vargreetingDidChange: ((GreetingViewModelProtocol) -> ())? { get set } // function to call when greeting did change
- init(person: Person)
- func showGreeting()
- }
- class GreetingViewModel : GreetingViewModelProtocol {
- let person: Person
- vargreeting: String? {
- didSet {
- self.greetingDidChange?(self)
- }
- }
- vargreetingDidChange: ((GreetingViewModelProtocol) -> ())?
- required init(person: Person) {
- self.person = person
- }
- func showGreeting() {
- self.greeting = "Hello"+ ""+ self.person.firstName +" "+ self.person.lastName
- }
- }
- class GreetingViewController : UIViewController {
- varviewModel: GreetingViewModelProtocol! {
- didSet {
- self.viewModel.greetingDidChange = { [unowned self] viewModel in
- self.greetingLabel.text = viewModel.greeting
- }
- }
- }
- let showGreetingButton = UIButton()
- let greetingLabel = UILabel()
- override func viewDidLoad() {
- super.viewDidLoad()
- self.showGreetingButton.addTarget(self.viewModel, action: "showGreeting", forControlEvents: .TouchUpInside)
- }
- // layout code goes here
- }
- // Assembling of MVVM
- let model = Person(firstName: "David", lastName: "Blaine")
- let viewModel = GreetingViewModel(person: model)
- let view = GreetingViewController()
- view.viewModel = viewModel
我们再来评估一下 MVVM 一系列的特征:
任务均摊 -- 在例子中并不是很清晰, 但是事实上, MVVM 的 View 要比 MVP 中的 View 承担的责任多因为前者通过 ViewModel 的设置绑定来更新状态, 而后者只监听 Presenter 的事件但并不会对自己有什么更新
可测试性 -- ViewModel 不知道关于 View 的任何事情, 这允许我们可以轻易的测试 ViewModel 同时 View 也可以被测试, 但是由于属于 UIKit 的范畴, 对他们的测试通常会被忽略
易用性 -- 在我们例子中的代码量和 MVP 的差不多, 但是在实际开发中, 我们必须把 View 中的事件指向 Presenter 并且手动的来更新 View, 如果使用绑定的话, MVVM 代码量将会小的多
总结
我们了解了集中架构模式, 希望你已经找到了到底是什么在困扰你毫无疑问通过阅读本篇文章, 你已经了解到其实并没有完全的银弹所以选择架构是一个根据实际情况具体分析利弊的过程
因此, 在同一个应用中包含着多种架构比如, 你开始的时候使用 MVC, 然后突然意识到一个页面在 MVC 模式下的变得越来越难以维护, 然后就切换到 MVVM 架构, 但是仅仅针对这一个页面并没有必要对哪些 MVC 模式下运转良好的页面进行重构, 因为二者是可以并存的
来源: https://juejin.im/entry/5aa1fd766fb9a028bd4be8f3