前端设计模式用起来(1)状态模式

4
业务代码开发久了,偶尔看看设计模式,总会让自己有一种清新脱俗的感觉。总想把这种感觉记下来,但一想到要先起个恰如其分的标题和开头,就让我有一种百爪挠心的纠结,所以迟迟没有开始。今天起更新我学习设计模式笔记的原因,就好像是,你喜欢一个女孩久了,却总不表白,难道不怕被别人截胡了么!

首先我们来一起设想一些场景:

  • 程序员们在等电梯的时候,聊天频率最高的一个话题,是不是电梯调度算法呢。写字楼的几部电梯到底是分单双层运力快,还是高低层运力快?当你按下电梯时,是就近楼层的电梯视来接你,还是自顾自的先上后下顺带来接你?
  • 开车来到路口,对红绿灯亮灭的长短是否曾有过习惯性的吐槽

除了电梯调度、红绿灯控制,软件设计和业务开发中,类似诸如状态切换的问题不难遇到。他们的共同点是:场景存在多个状态,状态改变时会触发对应的不同处理方法,状态间切换又存在诸多约束和限制

面对这种场景,你脑海里的第一解决方案是什么?条件分支if...else或者switch...case么?其实应当视具体场景复杂度来看,如果状态少逻辑简单,条件分支力所能及。但倘若状态较多,逻辑复杂,又存在诸多特殊情况的约束限制,本文将介绍的状态模式欢迎来解一下。

状态模式

定义:当一个对象的内在状态改变时,允许改变其行为,这个对象看起来像是改变了其类

我们先来看下传统面向对象语言的类图,后面再细说前端js中该如何应用
图片描述
对于一个if...else处理的长流程,我们可以抽象成多个状态的切换,不同具体的状态继承自一个抽象状态接口。场景类维护当前状态对象的一个实例,以及状态切换间的约束关系。

这样做的好处是将状态的获取和状态的切换进行了分离,一个具体的状态类只处理本状态相关的逻辑,符合单一职责原则,后期如果新加状态只需要新建具体的状态类,符合开放封闭原则

JavaScript没有抽象接口类的概念,所有类图也大大简化,如下:
状态模式UML类图
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...elseswitch...case都是有限状态机模型,只是平时没有意识到吧了。在处理较为复杂的逻辑时,考虑把业务逻辑抽象成一个有限状态机模型,常常会是代码逻辑清晰,结构规整。

有限状态机可以归纳出四个要素:

  1. 现态:即当前的状态。
  2. 条件:又称为“事件”。当一个条件被满足,将会触发一个动作,或者执行一次状态的迁移。
  3. 动作:条件满足后执行的动作。动作执行完毕后,可以迁移到新的状态,也可以仍旧保持原状态。动作不是必需的,当条件满足后,也可以不执行任何动作,直接迁移到新状态。
  4. 次态:条件满足后要迁往的新状态。次态是相对于现态而言的,次态一旦被激活,就转变成新的现态了。
Tips:避免把某个程序动作当作是一种状态来处理,动作是不稳定的,即使条件没有触发,一旦动作执行完也就结束了;但状态是稳定的,如果没有外部条件触发,状态会一直持续下去。

介绍了有限状态机,我们当然可以通过上面介绍的状态模式的方式,来将这种模型工具应用到我们的代码开发当中。但是你有没有注意到一个问题?对,代码不够优雅,略显简陋,不能忍!

接下来介绍一个优雅的有限状态机实现类库javascript-state-machine,接下来使用这个类库简单实现一个Promise的功能,来看一下如何使用。

Promise简单实现

首先回顾一下Promise的特点:

  • Promise是一个类。
  • Promise在实例初始化的时候需要传入一个函数。
  • 传入的函数需要接收resolvereject两个函数,成功的时候调用resolve,失败的时候调用reject
  • Promise实例出的对象有一个then方法,可以进行链式操作。
  • Promise拥有三种状态:pendingfulfilledrejected,可以从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的基本功能,演示了如何使用。

其实重点还是状态模式,通过本文介绍可以很明显地感受到其优点:

  1. 结构清晰,避免了过多的if...elseswitch...case的使用,降低了程序的复杂性,提高了系统的可维护性。
  2. 很好遵循了开放封闭原则和单一职责原则。
记得Martin在《重构》中,提到一个坏的代码味道“Long Method”,当你遇到一个方法中包含了一大堆逻辑,做了很多事的时候,你就应该嗅探到一股恶臭味,怎么去修改,或许考虑使用状态模式是一条途径。

但状态模式还有一点需要注意到,当采用子类继承实现多种具体状态的时候,注意控制状态的数量,以免出现子类数量膨胀的现象(在使用TypeScriptJava等更完整面向对象语言时)。

你可能感兴趣的

载入中...