設計模式-行爲模式之State

更多請移步 我的博客

意圖

State是行爲模式的一種,它允許你在對象內部狀態發生變化時改變其行爲。這個對象會改變它的類。

問題

狀態模式和有限狀態機很相似。

它主要的思想是程序是幾個狀態之一,這些狀態相互關聯。狀態的數量和它們之間的轉換是有限的並且是預先定義的。根據當前的狀態,程序對相同的事件會有不同的響應。

類似的方法可以應用到對象上。比如,Document(提案)對象可以是以下三種狀態之一:Draft(草案),Moderations(待審)和Publish(發佈)。對每種狀態而言,publish方法會有不同的處理方式:

  • 第一種狀態,它將改變提案的狀態到待審。

  • 第二種狀態,它將使提案變成發佈狀態,當然只有在當前用戶是管理員時。

  • 第三種狀態,它什麼也不做。

狀態機通常基於許多條件操作來實現,比如,if或者wsitch來檢查當前狀態並作出適當的行爲。即使你第一次聽說狀態機,你很可能已經至少實現多一次。下面這段代碼看起來是不是很眼熟?

class Document
string state;
// ...
method publish() {
    switch (state) {
        "draft":
            state = "moderation";
            break;
        "moderation":
            if (currentUser.role == 'admin')
                state = "published"
            break;
        "published":
            // Do nothing.
    }
}
// ...

當你你要添加更多狀態或者狀態依賴行爲到Document中時,使用條件構建起來的狀態機最大的限制就展露出來。大多方法需要有很多條件參數來決定這個狀態下的正確行爲。

這些代碼很難維護,因爲任何轉換邏輯的改變都需要在每個方法中對每個條件做雙重檢查。

項目越老這個問題往往越大,因爲在設計階段事先預測所有可能的狀態和轉換是相當困難的。因此,一個由少量條件構成的精簡狀態機隨着時間的推移會變的一團糟。

解決

狀態模式建議爲上下文對象的所有可能狀態創建新類,並將與狀態有關的行爲提取到這些類中。

上下文將會包含一個代表當前狀態的狀態對象。上下文將把執行交給狀態對象,而不是自己去處理。

爲了改變上下文對象,一個狀態對象可以將另一個狀態對象傳到上下文中。但是爲了讓狀態可以呼喚,所有的狀態類必須遵循相同的接口,並且上下文必須通過狀態對象的接口和它通信。

上面描述的結構看起來有些像Strategy模式,但是它們有一個關鍵點不同。在狀態模式中,上下文和特定的狀態都可以發起狀態間的轉換。

真實世界類比

智能手機

智能手機當前處於不同的狀態會有不同的行爲:

  • 當手機處於解鎖狀態,按下按鈕會執行一些列功能。

  • 當手機被鎖定,所有的按鈕都會展示解鎖畫面。

  • 當手機電量低,所有按鈕都會展示充電畫面。

結構

state.png

  1. Context(上下文)持有Concrete State(具體狀態)對象的引用,並把狀態相關的行爲委託給它。上下文通過狀態通用的接口和狀態對象協作。上下文必須報漏出一個方法來接收一個新狀態對象。

  2. State(狀態)爲具體狀態聲明通用的接口。它聲明的方法應當對所有的狀態都是有意義的。但是每個狀態應該提供不同的實現。

  3. Concrete State實現特定狀態的行爲。增加狀態基類避免在多個狀態中出現重複代碼。

    一個狀態可以持有上下文的引用。這不僅讓它可以訪問上下文數據,也提供了一種法器狀態轉變的方式。

  4. 上下文和具體的狀態都可以決定何時發起狀態轉換及決定轉換到什麼狀態。爲了處理轉換,一個新的狀態對象應當被傳給上下文。

僞代碼

在這個例子中,狀態模式依賴當前不同的播放器的狀態來控制播放器不同的行爲。Player主類持有一個狀態對象的引用,它被用來處理播放器的大多數工作。用戶的一些操作會使播放器轉換到另一個狀態。

狀態模式讓播放器改變其行爲而對其他對象無感知。轉換可以被播放器本身或者特定的狀態對象執行。

// Common interface for all states.
abstract class State is
    protected field player: Player

    // Context passes itself through the state constructor.
    // This may help a state to fetch some useful context
    // data if needed.
    constructor State(player) is
        this.player = player

    abstract method clickLock()
    abstract method clickPlay()
    abstract method clickNext()
    abstract method clickPrevious()


// Concrete states provide the special implementation for
// all interface methods.
class LockedState is

    // When you unlock a locked player, it may assume one of
    // the two states.
    method clickLock() is
        if (player.playing)
            player.changeState(new PlayingState(player))
        else
            player.changeState(new ReadyState(player))

    method clickPlay() is
        Do nothing.

    method clickNext() is
        Do nothing.

    method clickPrevious() is
        Do nothing.


// They can also trigger state transitions in the context.
class ReadyState is
    method clickLock() is
        player.changeState(new LockedState(player))

    method clickPlay() is
        player.startPlayback()
        player.changeState(new PlayingState(player))

    method clickNext() is
        player.nextSong()

    method clickPrevious() is
        player.previousSong()


class PlayingState is
    method clickLock() is
        player.changeState(new LockedState(player))

    method clickPlay() is
        player.stopPlayback()
        player.changeState(new ReadyState(player))

    method clickNext() is
        if (event.doubleclick)
            player.nextSong()
        else
            player.fastForward(5)

    method clickPrevious() is
        if (event.doubleclick)
            player.previous()
        else
            player.rewind(5)


// Player acts as a context.
class Player is
    field state: State
    field UI, volume, playlist, currentSong

    constructor Player() is
        this.state = new ReadyState(this)

        // Context delegates handling user's input to a
        // state object. Naturally, the outcome will depend
        // on what state is currently active, since all
        // states can handle the input differently.
        UI = new UserInterface()
        UI.lockButton.onClick(this.clickLock)
        UI.playButton.onClick(this.clickPlay)
        UI.nextButton.onClick(this.clickNext)
        UI.prevButton.onClick(this.clickPrevious)

    // Other objects must be able to switch Player's
    // active state.
    method changeState(state: State) is
        this.state = state

    // UI methods delegate execution to the active state.
    method clickLock() is
        state.clickLock()
    method clickPlay() is
        state.clickPlay()
    method clickNext() is
        state.clickNext()
    method clickPrevious() is
        state.clickPrevious()

    // State may call some service methods on the context.
    method startPlayback() is
        // ...
    method stopPlayback() is
        // ...
    method nextSong() is
        // ...
    method previousSong() is
        // ...
    method fastForward(time) is
        // ...
    method rewind(time) is
        // ...

適用性

  • 當你有一個對象在不同狀態下會有不同行爲時。狀態的數量有很多。狀態關聯的代碼經常改變。

    狀態模式建議隔離那些和狀態相關的代碼到不同的類中。源類被叫做“上下文”,它要持有這些狀態對象中的其中一個。它應當把工作委託給這個狀態對象。這種結構允許通過提供給上下文不同狀態對象的方式改變上下文的行爲。

  • 當一個類被大量的通過當前類字段值來改變方法行爲的條件污染。

    狀態模式把條件分支轉換成合適狀態類中的方法。然後需要依賴多態吧執行委託到關聯的狀態對象。

  • 在基於條件的狀態機中有大量重複代碼分佈在相似狀態和轉換中時。

    狀態模式允許你組合狀態類的層次,通過把通用的代碼移動到層級的基類中來減少重複。

如何實現

  1. 聲明那個類來扮演Context角色。這可能是一個存在的類,已經有了狀態依賴的相關代碼;或者是一個新類,如果狀態相關的代碼分佈在各個類中。

  2. 創建State接口。大多情況下,他將鏡像聲明在Context中的方法。

  3. 爲每個真正的狀態,創建一個State接口的實現。遍歷Context的方法,把所有和狀態有關的代碼放到對應的狀態類中。

  4. 添加一個State類型的引用字段到Context類中,並且需要提供一個可複寫這個字段值的public方法。

  5. 再一次遍歷Context中的方法,用狀態對象的相應方法替換剩餘的狀態條件部分。

  6. 爲了轉換Context的狀態,必須創建一個狀態實例傳給上下文。這步可有Context自己,或者通過State,或者Client來完成。注意,創建者將需要依賴具體的State類進行實例化。

優點

  • 消除狀態機條件。

  • 把與特定狀態相關的代碼組織到不同的類中。

  • 簡化代碼上下文。

缺點

  • 如果一個狀態機只有少數幾個狀態或很少發生變化,那麼這可能是過度設計。

和其他模式的關係

  • State,Strategy,Bridge(某種意義上的Adapter)有相似的解決結構。他們都共享“handle/body”元素。他們的意圖有所不同 - 也就是說,他們解決不同的問題。

  • State可以看作是Strategy模式的擴展。這兩種模式都使用組合把工作委託給輔助對象來改變主對象的行爲。但是,State模式允許狀態對象用另一個狀態改變當前上下文的狀態,使得它們相互依賴。

小結

State是一種行爲設計模式,允許對象在其內部狀態改變時改變行爲。該模式將與狀態相關的行爲提取爲單獨的狀態類,並強制原始對象將工作委派給狀態類的實例,而不是自行處理。

參考

翻譯整理自:https://refactoring.guru/design-patterns/state

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章