前端設計模式用起來(1)狀態模式

業務代碼開發久了,偶爾看看設計模式,總會讓自己有一種清新脫俗的感覺。總想把這種感覺記下來,但一想到要先起個恰如其分的標題和開頭,就讓我有一種百爪撓心的糾結,所以遲遲沒有開始。今天起更新我學習設計模式筆記的原因,就好像是,你喜歡一個女孩久了,卻總不表白,難道不怕被別人截胡了麼!

首先我們來一起設想一些場景:

  • 程序員們在等電梯的時候,聊天頻率最高的一個話題,是不是電梯調度算法呢。寫字樓的幾部電梯到底是分單雙層運力快,還是高低層運力快?當你按下電梯時,是就近樓層的電梯視來接你,還是自顧自的先上後下順帶來接你?
  • 開車來到路口,對紅綠燈亮滅的長短是否曾有過習慣性的吐槽

除了電梯調度、紅綠燈控制,軟件設計和業務開發中,類似諸如狀態切換的問題不難遇到。他們的共同點是:場景存在多個狀態,狀態改變時會觸發對應的不同處理方法,狀態間切換又存在諸多約束和限制

面對這種場景,你腦海裏的第一解決方案是什麼?條件分支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
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章