iOS 之 App 架构(二)

734 阅读8分钟

五种不同的App设计模式

  • 标准的Cocoa Model-View-Controller (MVC) 是Apple 在示例项目中所采用的设计模式。它是Cocoa app 中最为常⻅的架构,同时也是在Cocoa 中讨论架构时所采用的基准线。
  • Model-View-ViewModel+协调器(MVVM-C) 是MVC 的变种,它拥有单独的“view-model” (视图模型) 和一个用来管理view controller 的协调器。MVVM 使用数据绑定(通常会和响应式编程一起使用) 来建立view-model 层和view 层之间的连接。
  • Model-View-Controller+ViewState (MVC+VS) 这种模式将所有的view state 集中到一个地方,而不是让它们散落在view 和view controller 中。这和model 层所遵循的规则相同
  • Model 适配器-View 绑定器(Model Adapter-ViewBinder, MAVB) MAVB 专注于构建声明式的view,并且抛弃controller,采用绑定的方式来在model 和view 之间进行通讯。
  • Elm 架构(TEA) 与MVC 或者MVVM 这样的常⻅架构完全背道而驰。它使用虚拟view层级来构建view,并使用reducer 来在model 和view 之间进行交互。

Model-View-Controller

controller 层接收所有的view action,处理所有的交互逻辑,发送所有的model action,接收所有的model 通知,对所有用来展示的数据进行准备,最后再将它们应用到view的变更上去。

image.png

图中的虚线部分代表运行时的引用,view 层和model 层都不会直接在代码中引用controller。实线部分代表编译期间的引用,controller 实例知道自己所连接的view 和model 对象的接口。

构建

App 对象负责创建最顶层的view controller,这个view controller 将加载view,并且知道应该从model 中获取哪些数据,然后把它们显示出来。Controller 要么显式地创建和持有model层,要么通过一个延迟创建的model 单例来获取model。在多文档配置中,model 层由更低层的像是UIDocument 或NSDocument 所拥有。那些和view 相关的单个model 对象,通常会被controller 所引用并缓存下来。

更改Model

在MVC 中,controller 主要通过target/action 机制和(由storyboard 或者代码进行设置的) delegate 来接收view 事件。Controller 知道自己所连接的view,但是view 在编译期间却没有关于controller 接口的信息。当一个view 事件到达时,controller 有能力改变自身的内部状态,更改model,或者直接改变view 层级。

更改View

在我们所理解的MVC 中,当一个更改model 的view action发生时,controller 不应该直接去操作view 层级。正确的做法是,controller 去订阅model 通知,并且在当通知到达时再更改view 层级。这样一来,数据流就可以单向进行:view action 被转变为model 变更,然后 model 发送通知,这个通知最后被转为view 变更。

View State

View state 可以按需要被store 在view 或者controller 的属性中。相对于影响model 的view action,那些只影响view 或controller 状态的action 则不需要通过model 进行传递。对于view state 的存储,可以结合使用storyboard 和UIStateRestoring 来进行实现,storyboard负责记录活跃的controller 层级,而UIStateRestoring 负责从controller 和view 中读取数据。

测试

在MVC 中,view controller 与app 的其他部件紧密相连。边界的缺失使得为controller 编写单元测试和接口测试十分困难,集成测试是余下的为数不多的可行测试手段之一。在集成测试中,我们构建相连接的view、model 和controller 层,然后操作model 或者view,来测试是否能得到我们想要的结果。

Model-View-ViewModel+协调器

MVVM 和MVC 类似,也是通过基于场景(scene,view 层级中可能会在导航发生改变时切入或者换出的子树) 进行的架构。

相较于MVC,MVVM 在每个场景中使用view-model 来描述场景中的表现逻辑和交互逻辑。

View-model 在编译期间不包含对view 或者controller 的引用。它暴露出一系列属性,用来描述每个view 在显示时应有的值。把一系列变换运用到底层的model 对象后,就能得到这些最终可以直接设置到view 上的值。实际将这些值设置到view 上的工作,则由预先建立的绑定来完成,绑定会保证当这些显示值发生变化时,把它设定到对应的view 上去。响应式编程是用来表达这类声明和变换关系的很好的工具,所以它天生就适合(虽说不是严格必要) 被用来处理view-model。在很多时候,整个view-model 都可以用响应式编程绑定的方式,以声明式的形式进行表达。

在理论上,因为view-model 不包含对view 层的引用,所以它是独立于app 框架的,这让对于view-model 的测试也可以独立于app 框架。

由于view-model 是和场景耦合的,我们还需要一个能够在场景间切换时提供逻辑的对象。在MVVM-C 中,这个对象叫做协调器(coordinator)。协调器持有对model 层的引用,并且了解view controller 树的结构,这样,它能够为每个场景的view-model 提供所需要的model 对象。

和MVC 不同,MVVM-C 中的view controller 从来都不会直接引用其他的view controller (所以,也不会引用其他的view-model)。View controller 通过delegate 的机制,将view action的信息告诉协调器。协调器据此显示新的view controller 并设置它们的model 数据。换句话说,view controller 的层级是由协调器进行管理的,而不是由view controller 来决定的。

这些特性所形成的架构的总体结构如下图所示:

image.png

如果我们忽略掉协调器,那么这张图表就很像MVC 了,只不过在view controller 和model 之 间加入了一个阶段。MVVM 将之前在view controller 中的大部分工作转移到了view-model 中,但是要注意,view-model 并不会在编译时拥有对view controller 的引用。

View-model 可以从view controller 和view 中独立出来,也可以被单独测试。同样,view controller 也不再拥有内部的view state,这些状态也被移动到了view-model 中。在MVC 中view controller 的双重⻆色(既作为view 层级的一部分,又负责协调view 和model 之间的交互),减少到了单一⻆色(view controller 仅仅只是view 层级的一部分)。

协调器模式的加入进一步减少了view controller 所负责的部分:现在它不需要关心如何展示其他的view controller 了。因此,这实际上是以添加了一层controller 接口为代价,降低了view controller 之间的耦合。

构建

对于model 的创建和MVC 中的保持不变,通常它是一个顶层controller 的职责。不过,单独的model 对象现在属于view-model,而不属于view controller。初始的view 层级的创建和MVC 中的一样,通过storyboard 或者代码来完成。和MVC 不同的是,view controller 不再直接为每个view 获取和准备数据,它会把这项工作交给view-model。View controller 在创建的时候会一并创建view-model,并且将每个view 绑定到view-model所暴露出的相应属性上去。

更改Model

在MVVM 中,view controller 接收view 事件的方式和MVC 中一样(在view 和view controller 之间建立连接的方式也相同)。不过,当一个view 事件到达时,view controller 不会去改变自身的内部状态、view state、或者是model。相对地,它立即调用view-model 上的方法,再由view-model 改变内部状态或者model。

更改View

和MVC 不同,view controller 不监听model。View-model 将负责观察model,并将model的通知转变为view controller 可以理解的形式。View controller 订阅view-model 的变更,这通常通过一个响应式编程框架来完成,但也可以使用任意其他的观察机制。当一个view-model 事件来到时,由view controller 去更改view 层级。

为了实现单向数据流,view-model 总是应该将变更model 的view action 发送给model,并且仅仅在model 变化实际发生之后再通知相关的观察者。

View State

View state 要么存在于view 自身之中,要么存在于view-model 里。和MVC 不同,view controller 中不存在任何view state。View-model 中的view state 的变更,会被controller 观察到,不过controller 无法区分model 的通知和view state 变更的通知。当使用协调器时,view controller 层级将由协调器进行管理。

测试

因为view-model 和view 层与controller 层是解耦合的,所以可以使用接口测试来测试view-model,而不需要像MVC 里那样使用集成测试。接口测试要比集成测试简单得多,因为不需要为它们建立完整的组件层次结构。

为了让接口测试尽可能覆盖更多的范围,view controller 应当尽可能简单,但是那些没有被移出view controller 的部分仍然需要单独进行测试。在我们的实现中,这部分内容包括与协调器的交互,以及初始时负责创建工作的代码。