上篇讲述了增强逻辑功能测试而改进MVC为MVP,但是这样做可能还不够彻底,现在来讨论另一个纯粹从测试角度设计的框架。
首先我们来明确一下,测试中最核心的东西是什么。当然是数据,我们永远是围绕着数据来的,那么之前一些架构的问题是什么。无论哪个框架,数据的流通都是双向的,当数据流通成为单向了会怎么样呢?
1 | in data ==> Module ==> out data |
这样我们伪造数据进行测试就会非常方便了。按照这个思想就有了数据单向流通的架构。
数据单向流通的实现
这个概念最早是在web中提出的,应用在React里,官方的方案是Redux
。现在swift也提出了一种实现ReSwift
。
我在之前写React的时候使用过这种方案,从开发角度来说,这种方案会大大增加开发难度,代码量也会大量增加,而且开发思路也需要从以前的思考方式转换过来。但是如果我们把这个思路转换过来,其实对整个流程是更加简化和分离的。
从测试角度看,我觉得无疑是我知道的最可测的一种框架,甚至可以测试部分视图的逻辑。
那么总的来说,很难说这种结构的好坏,就算不考虑增加的开发时间,也是一种难以给以一种评价的方案。
(Redux/ReSwift)框架介绍
方案的几个核心是:
- 数据的单向流通
- 每个视图都可以看做一个状态机
- pure function
关于pure function,我就不做太多介绍了,简单的说,就是同一输入必定会有相同的输出,是非常容易测试的一种函数。
首先,我们来看一下官方的架构图。
可以看到,数据流动方向都是朝一个方向进行的。那么下面从每个模块来介绍下,还是以star button为例子。
State
视图状态机,也是所有会更新界面数据保存的地方,可以认为相当于ViewModel。
首先我们star会有以下几种视觉样式
1 | enum StarButtonState { |
所以State可以定义为
1 | struct StarState: StateType { |
Action
首先我们定义几种状态机转换的Action类型
1 | struct StarAction: Action { } |
以及相应的功能以及状态变更,这里异步请求采用延迟来代表。
1 | func star(id: String) -> Store<StarState>.ActionCreator { |
View
视图层其实很简单,只需要根据State的不同来更新就可以了。注意的是,更新都是无状态的,和上一个状态无关,所以view层是个无状态层。
1 | class StarButton: UIButton, StoreSubscriber { |
Reducer
状态转换器,唯一可以更新State的地方。
1 | func starReducer(action: Action, state: StarState?) -> StarState { |
数据传递
那么最重要的就是数据如何传递的了。首先要明确的是每个模块能够修改的,或者说是传递的,只能是下个模块。
比如,用户star button触发了一个事件:
1 | func onButton(sender: StarButton) { |
此时会创建Action,也就是将view事件转换为Action。然后会传递到store中,store会调用Reducer进行处理。Reducer更新state之后又会触发store的subscribe事件,回到view的func newState(state: StarState)
。
1 | View (User Event) |
大概的一个流程就是这样了。
接下来说说这样做的模块化的优势。
模块化和测试性
首先,我们需要有函数式编程的概念,函数也是一等公民,所以ActionCreator
和Reducer
都是独立的模块。
作为使用者,我们在不需要像MVC一样知道这些api所代表的操作功能,相对应的,我们需要去了解一个模块的动作(Action),比如以上例子就是
1 | func star(id: String) |
这样的划分比MVC要友好的多,真正的把逻辑功能从原本的C中分离开。需要触发这个行为也非常简单store.dispatch(star(id: id))
。相比MVP,行为更加的独立,每个行为之间完全没有联系,也不会产生干扰影响。同时因为每个行为的独立性,可复用程度也就越高。
Reducer则代表了view层的更新,也可以非常明确的知道每个状态的变更发生了什么。相比其他模式,将界面更新完全交给view或者Controller,Reducer是最明确也是最清晰的。同时Reducer也是独立的,可以替换的。
对于UIkit层面我们无法单元测试,所以测试的主要部分是Action
和Reducer
。这两个模块可以说都是pure function
或者在某些条件下是pure function
的,所以测试也非常的简单。
对比
和这个模式比较像的有状态机模式和Reactive。
状态机模式也是实现对应功能,以及对应状态,然后通过子类化的方式去实现Reducer的功能。
Reactive则比较像ActionCreator,只是Reactive返回的是信号量。
使用场景
从上面可以看出这是一套非常优秀的模块划分方案,但同时也会大大增加代码量,而且需要改变以前的思维模式。而对于目前国内的现状来看,很难有这么多时间和精力让整个项目都使用这种模式。
但是这种模式的特点也非常的明显,在处理比较复杂的交互行为,并且存在较多的视图状态的时候,会是一种比较好的方案。比如视频播放界面。
所以个人认为,在一些简单的场景下并不需要使用该方案,但是在一些复杂的交互页面,而且又非常想要引入单元测试的场景,可以酌情考虑下这种方案。这种方案要求人们的思维方式的改变,需要有一定的函数式编程的概念。
虽然不一定会直接使用ReSwift,但是这种思想有很多值得借鉴的地方,利用这种思想做出类似的效果,以便达到可以容易进行白盒测试的目的。
最后
以上虽然说不会全部使用该方案,但也可以部分使用。比如独立的小模块,亦或是app层面的一些东西。下次可以讨论下app层面如何来利用单向数据流来简化流程。