Swift 状态机

过载(overloaded)

对象需要表示的信息: 类型、身份、行为、状态、值

对象具有的用来表示信息的方法: 类、实例化、方法、属性

这就带来了一个问题,属性需要用来表示状态和值两种信息。

下面是一段喜闻乐见的代码:

1
2
3
4
5
6
7
8
9
var fetchTask: URLSessionTask?
func fetchThing(params:NSDictionary){
if fetchTask == nil{
fetchTask = myAPI.getRequest(params){
//...
}
}
}

这段代码的意思是:如果现在网络请求,就发起一个。

看起来可能没有什么问题,但这确会是一场灾难。比如说我们需要添加更多的 CRUD 操作,比如说做一个保存的功能,这时候需要发起一个 POST 请求。我们并不需要在请求完成之前在获取到新的数据:

1
2
3
4
5
func fetchThing(params:NSDictionary){
if fetchTask == nil && saveTask == nil{
...
}
}

这段代码虽然变丑了,但依然是可控的。但是如果再增加一些操作呢?这个判断可能最多会变成 2^n 个分支了。

补足

这个问题的核心问题在于 fetchTask 在这里其实是有两种用途的。我们使用它要表示一个值(网络请求任务),也用它来表示一种状态(当前是否在发起请求)。这两种的关联程度是很高的,这种事情其实经常会发生。

savaTask 的时候,这个属性在表示了一个任务的同时也表示了另外的一种状态,这时候,就不仅仅是值和状态在同一个属性的问题了,在这种情况下,状态还分散在了多个不同的属性中。

解决的办法就是抽象,因为我们只有属性这一个锤子,值和状态就像是两个钉子,如果我们再引入一个新的东西来表示状态这个问题就解决了。

这个抽象长什么样呢?

状态机

Wiki 百科有状态机的介绍, 这篇文章将不会在概念上做过多的描述。总的来说,状态机是一种抽象的数据结构。他是一组状态,和这些状态之间的转换规则。

举个例子,现在有 5 种状态: ReadyFetchingSavingSuccessfailure

我们需要一个机器来模型化这 5 中可能的状态,以及这五种状态之间的转换规则。(比如: 可以从 Ready 转换到 Fetching 但不能从 Saving 转换到 Fetching, 也不能从 Fetching 转到 Fetching)

然后我们不仅有了一个规范的位置来找到对象不同的状态,而且我们在状态机种构建出来的规则也能避免对象进入无效的状态。这样的话,管理某个对象的状态就会变的更简单。

Swift 状态机

使用现有的状态机库的最大障碍是让他们可配置 。在让我们从这些东西中解放出来之前,我们首先需要定义所有的状态,以及这些状态之间的转换。这需要很多很丑的工厂,多余的样板,甚至是 XML。

首先我们来定义这个状态机:

1
2
3
4
5
6
7
8
9
10
11
class StateMachine {
// 稍后会来定义相关的状态
var state: StateType
init(_ initialState: StateType) {
self.state = initialState
}
}

接下来我们会定义一些代理,首先我们需要知道定义这个代理是用来做什么的:

  • 告诉我们当前的状态是什么
  • 决定新的状态是否合理
  • 在状态转换之后执行什么任务(可以没有任务)
1
2
3
4
5
6
protocol StateMachineDelegateProtocol: AnyObject {
associatedtype StateType
func shouldTransitionFrom(from:StateType, to:StateType)->Bool
func didTransitionFrom(from:StateType, to:StateType)
}

需要注意的是⚠️,

  • didTransitionFrom(to:) 方法可能需要被标记为 optional
  • 这个协议应该是 class-only 的。这也是代理协议的习惯写法。从哲学层面上将,值语义的代码并不会有什么意义。
  • 代码属性几乎全部都应该用 weak 或者 unowned 修饰。

接下来我们把这个协议加入状态机中:

1
2
3
4
5
6
7
8
9
10
11
12
class StateMachine<P: StateMachineDelegateProtocol>{
private unowned let delegate: P
var state: P.StateType
init(_ initialState: P.StateType, delegate: P) {
self.state = initialState
self.delegate = delegate
}
}

需要切换状态,以及成功切换状态之后就必须要调用代理的方法。

Swift 不能在储存属性的 setter 方法中设置这个属性的值,所以我们还需要一个公有的计算属性来做类似的操作。

1
2
3
4
5
6
7
8
9
10
11
12
private var _state: P.StateType
var state: P.StateType {
get {
return _state
}
set {
if delegate.shouldTransitionFrom(from: self.state, to: newValue) {
self.state = newValue
}
}
}

然后再修改切换状态之后调用另外一个代理方法

1
2
3
4
5
private var _state: P.StateType {
didSet {
delegate.didTransitionFrom(from: oldValue, to: _state)
}
}

最后,由于设置这个状态的初始值并不是我们概念中的状态转换,所以并不需要在这个时候调用 shouldTransitionFrom(to) 方法。所以在状态机的初始化方法中,我们直接给 _state 这个储存属性复制。最后代码长这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class StateMachine<P: StateMachineDelegateProtocol> {
private unowned let delegate: P
private var _state: P.StateType {
didSet {
delegate.didTransitionFrom(from: oldValue, to: _state)
}
}
var state: P.StateType {
get {
return _state
}
set {
if delegate.shouldTransitionFrom(from: self.state, to: newValue) {
self.state = newValue
}
}
}
init(_ initialState: P.StateType, delegate: P) {
self._state = initialState
self.delegate = delegate
}
}

传统观念

上面的状态机看起来还不错吧,但是应该怎么用呢?

由于这里的代理方法是很通用的,如何使用其实是没有标准答案的,但是从我稍前的经验看来, Swift 有很多还不错的工具来帮我们实现这个,如枚举、元组、switch 语句。

首先要做的是定义我们需要的状态:

1
2
3
4
5
6
7
8
class MyClass: StateMachineDelegateProtocol {
enum AsyncNetworkState {
case ready, fetching, saving
case success([String: Any])
case failure(Error)
}
typealias StateType = AsyncNetworkState
}

然后,通过Swift 以及元组,来实现 shouldTransitionFrom(to:) 方法

1
2
3
4
5
6
7
8
9
10
11
12
func shouldTransitionFrom(from: MyClass.AsyncNetworkState, to: MyClass.AsyncNetworkState) -> Bool {
switch (from, to) {
case (.ready, .fetching), (.ready, .saving):
return true
case (.fetching, .success), (.fetching, .failure):
return true
case (.saving, .success), (.saving, .failure):
return true
case (_ , .ready):return true
default: return false
}
}

我们先看下面的代码,default 语句直接让那些没有明确定义好的状态之间的切换永远不会发生。

再看上面一点,从准备状态到其他任何异步状态都是允许的,然后从任何的异步状态转换到结果状态也都是允许的。在这里我把他们分别写在不同的case 里面只是为了代码的可读性。

最后所有其他状态转换到准备状态都是允许的。

使用状态

我们使用类似的方法来实现另外一个方法:

1
2
3
4
5
6
7
8
9
func didTransitionFrom(from: MyClass.AsyncNetworkState, to: MyClass.AsyncNetworkState) {
switch (from, to) {
case (.ready, .fetching):
task = myAPI.fetchRequestWithCompletion{ ... }
case (.ready, .saving):
task = myAPI.saveRequestWithCompletion{ ... }
default: break
}
}

这这里,当状态从准备转换到任何的异步状态的时候,会分别发起对应的请求。还记得最开始的情况吗?我们现在已经把状态的表示从 task 这个属性中剥离出来了。现在我们其实可以在 fetching 的事件中写一行:

1
2
3
func fetchThings(params: [String: Any]) {
machine.state = .fetching
}

因为上文中我们已经提到过从 .fetching.fetching 是不允许的转换,所以无论这个方法被调用了多少次,也只可能有一个网络请求被发起。

还有一个好处是,我们可以使用枚举的关联值来传递数据,进一步改造,相应的方法应该是这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
func didTransitionFrom(from: MyClass.AsyncNetworkState, to: MyClass.AsyncNetworkState) {
switch (from, to) {
case (.ready, .fetching):
myAPI.fetchRequestWithCompletion{json, error in
if let someError = error{
machine.state = .Failure(someError)
} else{
machine.state .Success(json)
}
}
case (_, .failure(let error)):
displayGeneralError(error)
machine.state = .Ready
case (.fetching, .success(let json)):
parseFetchSpecificJSON(json)
machine.state = .Ready
case (_, .ready):
updateInterface()
default: break
}
}

网络请求的结果回调,跟其他的事件一样,也能够被简化成状态的转换。这样的话,程序的交互就会变得非常的简单了,我们不需要再去关注是什么行为导致了界面的更新,看看到底是什么状态更新了UI是很简单的问题。

CepheusSun wechat
订阅我的公众号,每次更新我都不一定会告诉你!
坚持原创技术分享,您的支持将鼓励我继续创作!
0%