四、MVC设计模式

Reference:
App Architecture

MVC基于经典的面向对象原则:对象在内部对它们的行为和状态进行管理,并通过类和协议的接口进行通讯。MVC中view对象通常是自包含且可重用的model对象独立于表现形式之外,且避免依赖程序的其他部分;controller层负责将model层和view层撮合到一起工作,controller对另外两层进行构建和配置,并对model对象和view对象之间的双向通讯进行协调

MVC中APP的反馈回路

iOS中标准的MVC是三种不同的子模式的集合:

  1. 组合模式:view被组装成为层级,该层级按组区分,由controller对象进行管理
  2. 策略模式:controller对象负责协调view和model,并且对可重用的、独立于app的view在app中的行为进行管理
  3. 观察者模式:依赖于model数据的对象必须订阅和接收更新

在所有模式中,MVC通常都是代码量最少,设计开销最小的模式。MVC模式有两个主要的缺陷,其一是view controller拥有太多的职责,所导致的“massive view controller”;同时MVC也难以测试,特别是单元测试和接口测试非常困难,而比较可行的集成测试的编写也并非易事。

一、MVC的实现

1.构建

Cocoa MVC程序的创建过程会完成对三个对象的创建:UIApplication对象,Application delegate,以及主窗口的根ViewController
这三个对象的默认配置文件分别为:Info.plist,AppDelegate和Main.storyboard
同时它们又提供了对后续启动流程进行配置的地方,这三个对象都属于Controller层级,所以说Controller层负责了所有的构建工作。

MVC app中的View不直接引用model对象,View将保持独立可重用,model对象将被存储在ViewController中,ViewController将变成了个不可重用的类,model对象赋予了ViewController对应的身份。

2.将View连接到数据

在MVC中Controller根据初始的model对View进行配置,并观察model,在model的变更通知到到达时做相应的View变更。

在ViewController上设置一个初始model值时,有三种不同的方式:

  1. 通过判定controller在controller层级上的位置以及controller的类型,直接访问一个全局的model对象
  2. 开始时将model对象的引用设置为nil并让所有东西保持为空白状态,直到另一个controller提供了一个非nil值
  3. 在controller初始化时将model对象当作参数传递进来(也就是依赖注入)

在构建阶段结束后,对于view层级的变更应该遵循MVC中观察者模式的部分,只发生在观察的回调中

观察者模式是在MVC中维持model和view分离的关键。这种方式的优点在于,不论变更究竟是源自哪里(比如,view事件、后台任务或者网络),我们都可以确信UI是和model数据同步的。而且,在遇到变更请求时,model将有机会拒绝或者修改这个请求,即使model被以其他方式(比如一个网络事件)改变,或者是model拒绝这次变更时,UI也会正确更新。这是一种确保view层始终与model层同步的十分健壮的方式

3.更改Model

View Action的事件回路:

  1. view通过target/action机制和delegate等方式将View Action传递给ViewController;
  2. ViewController接收到变更model的View Action时直接更改model;
  3. ViewController通过观察者模式响应model变更并更新view。

4.更改ViewState

MVC的model层起源于典型的基于文档的app:任何在保存操作中写入文档的状态都被当作是model的一部分来考虑。其他的任意状态(包括像是导航状态,临时的搜索和排序值,异步任务的反馈以及未提交的更改)传统意义上是被排除在MVC的model定义之外的,我们把这些状态统称为view state。在MVC中,这些被我们统称为view state的“其他”状态没有被包含在模式的描述中。

依照传统的面向对象的原则,任意的对象都可以拥有内部状态,这些对象也不需要将内部状态的变化传达给程序的其余部分。基于这种内部处理,view state不需要遵守任何一条程序中的清晰路径。任意view或者controller都可以包含状态,这些状态由View Action进行更新。view state的处理尽可能地在本地进行:一个view或者ViewController可以独自响应用户事件,对自身的view state进行更新

如果想要在view层级的不同部分共享view state,有两种方式:

  1. 找到它们共同的祖先,并在那儿管理状态
  2. 使用单例来管理状态

实际操作中,将view state放到层级的顶层controller对象中的做法(即第一种方式)并不常⻅,因为这会要求层级的每一层之间存在通讯的管道。

5.测试

自动测试可以由好几种不同形式进行。从最小的粒度到最大的粒度,它们包括:

  • 单元测试:将独立的函数独立出来,并测试它们的行为
  • 接口测试:使用接口输入并测试接口输出得到的结果,输入和输出通常都是函数
  • 集成测试:在整体上测试程序或者程序的主要部分

单元测试只花了十多年的时间就成为了app中最常⻅的测试方式。不过即使是现在,许多app除了人工测试以外并没有进行常规的自动测试。如果想要在代码层级对MVC的controller和model层进行自动测试,可行的选项是写集成测试,但想要正确书写集成测试,需要大量关于Cocoa框架是如何操作的知识,这并非易事。在MVC中,我们需要从window开始构建ViewController树和view树,在树构建出来后便可以选择以新创建出来的树作为单个集成测试单元进行测试:书写代码触发事件,然后与我们的预期进行验证。

二、MVC的问题

观察者模式失效

第一个问题是,model和view的同步可能失效。当围绕model的观察者模式没有被完美执行时,这个问题就会发生。常⻅的错误是,在构建view时读取了model的值,而没有对后续的通知进行订阅。另一个常⻅错误是在变更model的同时去更改view层级。解决方法只有严格地遵守观察者模式:当读取model值时,也需要对它进行订阅。

肥大的ViewController

ViewController需要负责处理view层(设置view属性,展示view等),但是它同时也负责controller层的任务(观察model以及更新view),最后,它还要负责model层(获取数据,对其变形或者处理)。结合它在架构中的中心⻆色,这使得我们很容易在不经意间把所有的职责都赋予ViewController,从而迅速让程序变得难以管理。

关于肥大的ViewController最主要的争论不是关于代码的行数,而在于所保存的状态的数量。当整个文件就像ViewController这样是一个单独的类的时候,所有的可变状态都将被文件中的各个部分共享,每个函数需要精诚合作,来共同读取和维护这些状态,避免彼此矛盾,这无疑是会让代码的可读性和可维护性都越来越差。

三、MVC的改进

观察者模式

最好是有一种方式,能让对View的初始设定和在viewDidLoad中对model建立观察者之间没有时间空隙。使用键值观察(KVO)来替代通知是可选项之一,但是,在大部分情况下这通常需要同时观察多个不同的键路径,这让KVO和通知的方式相比,实际上并没有更加稳定。在Swift中KVO由于需要每个被观察的属性都声明为dynamic,这也让它在Swift中远远没有在Objective-C中那样流行。

对观察者模式最简单的改进方式是,将NotificationCenter进行封装并为它实现KVO所包含的初始化的概念。这个概念会在观察被建立的同时发送一个初始值,这允许我们将设定初始值和观察后续值的操作合并到一个单一管道中去。另外一种可行的思考是,将观察者模式替换为在MVVM中比较常用的响应式编程,这样可以借助一些响应式编程框架,将对初始值的设定与后续观察都合并在一次订阅操作中,同时也保证了model与view的同步,例如在ReactiveCocoa中,对KVO的封装也让这类代码的书写非常地方便。

肥大ViewController的问题

ViewController主要的工作为:观察model,展示view,为它们提供数据,以及接收View Action。造成肥大ViewController的原因就是ViewController通常还负责了主要工作之外的无关工作,这些额外的工作有的应该被分散到较小层级的Controller中,这些Controller各自管理一个较小部分的view;有的则可以通过接口和抽象将其封装起来,作为工具类使用;还有的功能则是应该移动到model层中,比如排序、数据获取和处理等方法,它们与app的数据和专用逻辑相关,把它们放在model中会是更好的选择。

所以解决肥大ViewController的主要思路有:

  • 使用较小层级的Controller
    通过将主要的view的逻辑分散到它们自己的更小的controller中,简化场景的view依赖,在父ViewController中剩下的工作就只有集成和布局了
  • 将部分任务封装成工具类
    可以创建工具类来执行像是获取用户位置信息这种异步任务,这样,在controller中需要的代码就只是创建任务和回调闭包
  • 将数据相关的逻辑放在model中
    例如排序、数据获取和处理等方法,包括数据网络层的获取与处理,交由model管理将更加的合理

属于各个部件的逻辑应当尽可能地集成到各自的部件中去,将部分ViewController中的代码抽离出来,在本质上并没有降低整个程序的复杂度,但是这么做确实降低了ViewController本身的复杂度。

降低ViewController的复杂度的其他手段还包括:

  1. 使用代码而不是segue
    如果不使用segue,而是选择用代码来定义view层级,能在构建阶段有更多的控制力,其中最大的优点在于,可以更好地掌控依赖,这样可以让页面跳转时对ViewController的配置更加的方便可控。

  2. 在扩展中进行代码重用
    在不同的ViewController间共享代码,最常⻅的方法是创建一个包含共通功能的父类,另一种选择是使用扩展(类别)。相较于继承,这种重用代码的方式,在降低ViewController复杂度的同时,并没有提高继承链的复杂度。

  3. 利用ChildViewController进行代码重用
    ChildViewController是在ViewController之间共享代码的另一种选项,它符合使用较小层级Controller的思路,是降低ViewController复杂度同时提高代码重用的有效方式。

  4. 提取协调controller
    许多大的ViewController都有很多⻆色和职责,除了一些具体的任务可以抽离成工具类之外,其他的一些职责事务也可以通过对象提取出来,对这些提取出来的事务对象做泛型抽象后,还能在其他有相似事务的ViewController中重用

    苹果将controller的主要职责分为协调controller(coordinating controller)和调解controller(mediating controller)。一个协调controller是app特定的,而且一般来说是无法重用的(即ViewController的主要工作部分)。调解controller则是一个可重用的controller对象,通过配置,它可以被用来执行特定的任务。通常,用来遵守某个协议的代码(比如ViewController中遵守UITableViewDataSource的部分),比较适合被提取为调解controller,再将提取出来的对象进行泛型抽象,让部分差异化逻辑通过block等方式从外面传递进来,这个提取出来的对象,便能用于有相似调解controller逻辑的ViewController中。需要注意的是,分离controller的逻辑能降低ViewController的复杂度,但是当两个部件紧密耦合在一起,并且需要对很多状态进行通讯和共享的时候,分离后所带来的开销可能会非常大,反而使得事情变得更加复杂,这时不分离反而是更好的选择。

  5. 简化View配置代码
    如果ViewController需要构建和更新非常多的view,那么将这部分view配置的代码提取出来,对于降低ViewController的复杂度也非常有效,特别是对于那些不需要双向通讯的配置

-------------This article is over, thank you for reading -------------