一种基于状态机的代码设计

最近在学习 SwiftUI ,写了一个计算器的 Demo。写完后学习并理解到一种代码设计,感觉很有收获,在此记录一下。

一句话概括

我们将 App 当作一个状态机,状态机的状态决定 App 的界面。首先我们理出 App 都有什么状态?每个状态需要什么样的条件会触发什么样的 Action?Action 执行后状态机会进入什么样的次态?理清这些后,根据设计,随着状态机状态的改变,,App 的界面自然会跟着更新。

以计算器为例

在这里我用我写的计算器 Demo 为例讲解下这种方式。

找出状态

计算器的业务逻辑就是接受用户的输入的算式,然后计算并输出算式的结果。那么计算器的状态就是由算式的状态来决定。

首先看看算式的组成:

“左侧数字 + 计算符号 + 右侧数字 + 计算符号或者等号”

看着算式的结构,我们就能捋出计算器都有什么状态:

  1. 计算器正在输入左侧数字,这个状态下用户按了计算符号按钮后会进入下一个状态。
  2. 计算器已经输入了左侧数字和计算符号,开始等待右侧符号
  3. 计算器已经输入了左侧数字和计算符号,正在接收右侧数字(注意和 2 的区别,一个是符号,一个是数字
  4. 计算器已经计算出结果,开始等待符号(可能为新的计算,也可能以此次结果继续进行计算)
  5. 输入或计算出现错误,无法继续

所以我们需要在代码中定义出五个状态:

1
2
3
4
5
6
7
8
// 这五个枚举和上面定义的状态按顺序一一对应
enum CalculatorState {
case left(String)
case leftOp(left: String, op: Calculator.Op)
case leftOpRight(left: String, op: Calculator.Op, right: String)
case equal(value: String)
case error
}

状态我们已经定义出来了,接下来就需要定义改变状态时需要的 Action 了。

需要什么样的 Action 才能进入状态

这里主要以状态 1(left)为例,讲解在状态 1 下遇到什么条件会触发什么样的 Action,然后会进入哪个状态。

  1. 如果接收到了数字的输入,此时状态并不会发生改变,还是处于 left,只不过这个状态下的数据会发生改变
  2. 如果接收到了运算符的输入,此时就会进入状态 2(leftOp),而且状态 2 的数据是状态 1 的值加上刚刚输入的运算符(Calculator.Op)
  3. 如果接收到了等号,一个算式的生命周期会结束,进入状态 4(equal),等待重新开始一次计算。

语言是苍白的,直接看代码:

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
28
// 由于代码过长,全部附上会影响篇幅,此处仅附上部分代码。详细代码可以在文末找到链接
func apply(item: CalculatorButtonItem) -> CalculatorState {
switch item {
case .digit(let num):
return apply(num: num)
case .dot:
return applyDot()
case .op(let op):
return apply(op: op)
case .command(let command):
return apply(command: command)
}
}

private func apply(num: Int) -> CalculatorBarin {
switch self {
case .left(let left):
return .left(left.apply(num: num))
case .leftOp(let left, let op):
return .leftOpRight(left: left, op: op, right: "0".apply(num: num))
case .leftOpRight(let left, let op, let right):
return .leftOpRight(left: left, op: op, right: right.apply(num: num))
case .equal(_):
return .left("0".apply(num: num))
case .error:
return .left("0".apply(num: num))
}
}

上面只解释了状态 1 进入次态需要的条件和进入次态前会做的 Action,其他的状态其实都是类似的,此处就不再赘述。

其他场景

计算器的业务比较简单,用来阐述代码设计会清晰易懂一些。这种代码设计并不是万金油,哪里都能用,不过肯定也有很多比较适用的场景。比如应用商店的详情页面,详情页面的状态就比较多,而且状态和状态之间的区别比较清晰,用这种方式来处理就比较合适。

局限

当然这种设计也是有他的局限所在。

比如当业务比较复杂的时候可能会因为发生状态数量爆炸,此时代码应该就会失控了。其他的缺陷暂时还没有想到,等我进行一次最佳实践后可能会发现更多的问题。

天底下不存在一个绝对正确的设计,都需要根据实际情况自己做出相应的调整,以不变应万变~

最后

本文完整代码地址:https://github.com/T-Oner/SwiftUILearn

我在学习 SwiftUI,如何你也对这个感兴趣,那么我推荐这本我在用的教材:

扫描二维码查看教材

本文仅为个人意见,不构成编码建议。