业务代码开发久了,偶尔看看设计模式,总会让自己有一种清新脱俗的感觉。总想把这种感觉记下来,但一想到要先起个恰如其分的标题和开头,就让我有一种百爪挠心的纠结,所以迟迟没有开始。今天起更新我学习设计模式笔记的原因,就好像是,你喜欢一个女孩久了,却总不表白,难道不怕被别人截胡了么!
首先我们来一起设想一些场景:
- 程序员们在等电梯的时候,聊天频率最高的一个话题,是不是电梯调度算法呢。写字楼的几部电梯到底是分单双层运力快,还是高低层运力快?当你按下电梯时,是就近楼层的电梯视来接你,还是自顾自的先上后下顺带来接你?
- 开车来到路口,对红绿灯亮灭的长短是否曾有过习惯性的吐槽
除了电梯调度、红绿灯控制,软件设计和业务开发中,类似诸如状态切换的问题不难遇到。他们的共同点是:场景存在多个状态,状态改变时会触发对应的不同处理方法,状态间切换又存在诸多约束和限制。
面对这种场景,你脑海里的第一解决方案是什么?条件分支if...else
或者switch...case
么?其实应当视具体场景复杂度来看,如果状态少逻辑简单,条件分支力所能及。但倘若状态较多,逻辑复杂,又存在诸多特殊情况的约束限制,本文将介绍的状态模式欢迎来解一下。
状态模式
定义:当一个对象的内在状态改变时,允许改变其行为,这个对象看起来像是改变了其类
我们先来看下传统面向对象语言的类图,后面再细说前端js中该如何应用
对于一个if...else
处理的长流程,我们可以抽象成多个状态的切换,不同具体的状态继承自一个抽象状态接口。场景类维护当前状态对象的一个实例,以及状态切换间的约束关系。
这样做的好处是将状态的获取和状态的切换进行了分离,一个具体的状态类只处理本状态相关的逻辑,符合单一职责原则,后期如果新加状态只需要新建具体的状态类,符合开放封闭原则。
JavaScript没有抽象接口类的概念,所有类图也大大简化,如下:State
类为状态类,包含状态值以及状态改变时具体的处理方法。Context
类为场景类,维护状态间的约束关系以及控制触发状态的切换。
下面我们看下代码,让概念平稳落地:
// 定义状态类
class State {
constructor(color) {
this.color = color
}
// 处理该状态下具体逻辑
handle(context) {
console.log(`turn to ${this.color}`)
context.setState(this)
}
}
// 定义场景类
class Context {
constructor() {
this.state = null
}
setState(state) {
this.state = state
}
getState() {
return this.state
}
}
测试代码如下:
const ctx = new Context()
// 实例出具体状态
const red = new State('red')
const green = new State('green')
const yellow = new State('yellow')
// 绿灯亮
green.handle(ctx)
console.log(ctx.getState())
// 红灯亮
red.handle(ctx)
console.log(ctx.getState())
// 黄灯亮
yellow.handle(ctx)
console.log(ctx.getState())
有限状态机
状态模式脱胎自有限状态机(Finite-State-Machine),这个数学模型描述了有限个状态,以及这些状态之间转移和动作的行为,是一种对象行为建模的工具。类似下图就是一种有限状态机:
其实我们在处理业务逻辑时,经常打交道的各种事件和状态切换,写的各种if...else
和switch...case
都是有限状态机模型,只是平时没有意识到吧了。在处理较为复杂的逻辑时,考虑把业务逻辑抽象成一个有限状态机模型,常常会是代码逻辑清晰,结构规整。
有限状态机可以归纳出四个要素:
- 现态:即当前的状态。
- 条件:又称为“事件”。当一个条件被满足,将会触发一个动作,或者执行一次状态的迁移。
- 动作:条件满足后执行的动作。动作执行完毕后,可以迁移到新的状态,也可以仍旧保持原状态。动作不是必需的,当条件满足后,也可以不执行任何动作,直接迁移到新状态。
- 次态:条件满足后要迁往的新状态。次态是相对于现态而言的,次态一旦被激活,就转变成新的现态了。
Tips:避免把某个程序动作当作是一种状态来处理,动作是不稳定的,即使条件没有触发,一旦动作执行完也就结束了;但状态是稳定的,如果没有外部条件触发,状态会一直持续下去。
介绍了有限状态机,我们当然可以通过上面介绍的状态模式的方式,来将这种模型工具应用到我们的代码开发当中。但是你有没有注意到一个问题?对,代码不够优雅,略显简陋,不能忍!
接下来介绍一个优雅的有限状态机实现类库javascript-state-machine,接下来使用这个类库简单实现一个Promise的功能,来看一下如何使用。
Promise简单实现
首先回顾一下Promise
的特点:
-
Promise
是一个类。 -
Promise
在实例初始化的时候需要传入一个函数。 - 传入的函数需要接收
resolve
和reject
两个函数,成功的时候调用resolve
,失败的时候调用reject
。 -
Promise
实例出的对象有一个then
方法,可以进行链式操作。 -
Promise
拥有三种状态:pending、fulfilled、rejected,可以从pending->fulfilled,或pending->rejected,但不能逆向。
接下来上代码实现一下
// 状态机模型
const fsm = new StateMachine({
// 初始状态
init: 'pending',
// 状态迁移规则,name,from,to的名字尽量别同名
transitions: [
{ name: 'resolve', from: 'pending', to: 'fulfilled' },
{ name: 'reject', from: 'pending', to: 'rejected'}
],
methods: {
onResolve(state, data) {
data.successFn.forEach(fn => fn())
},
onReject(state, data) {
data.failFn.forEach(fn => fn())
}
}
})
// 定义Promise
class MyPromise {
constructor(fn) {
this.successFn = []
this.failFn = []
fn(
() => { fsm.resolve(this)},
() => { fsm.reject(this)}
)
}
then(successFn, failFn) {
this.successFn.push(successFn)
this.failFn.push(failFn)
return this
}
}
总结
本文介绍了状态模式和有限状态机的概念,以及才开发中优雅使用的姿势javascript-state-machine
,并通过用其简单实现了Promise的基本功能,演示了如何使用。
其实重点还是状态模式,通过本文介绍可以很明显地感受到其优点:
- 结构清晰,避免了过多的
if...else
或switch...case
的使用,降低了程序的复杂性,提高了系统的可维护性。 - 很好遵循了开放封闭原则和单一职责原则。
记得Martin在《重构》中,提到一个坏的代码味道“Long Method”,当你遇到一个方法中包含了一大堆逻辑,做了很多事的时候,你就应该嗅探到一股恶臭味,怎么去修改,或许考虑使用状态模式是一条途径。
但状态模式还有一点需要注意到,当采用子类继承实现多种具体状态的时候,注意控制状态的数量,以免出现子类数量膨胀的现象(在使用TypeScript
或Java
等更完整面向对象语言时)。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。