Go設(shè)計(jì)模式之狀態(tài)模式圖文詳解
狀態(tài)模式 (State)
狀態(tài)模式是一種行為設(shè)計(jì)模式, 讓你能在一個(gè)對(duì)象的內(nèi)部狀態(tài)變化時(shí)改變其行為, 使其看上去就像改變了自身所屬的類一樣。
問題
狀態(tài)模式與有限狀態(tài)機(jī) 的概念緊密相關(guān)。

其主要思想是程序在任意時(shí)刻僅可處于幾種有限的狀態(tài)中。 在任何一個(gè)特定狀態(tài)中, 程序的行為都不相同, 且可瞬間從一個(gè)狀態(tài)切換到另一個(gè)狀態(tài)。 不過, 根據(jù)當(dāng)前狀態(tài), 程序可能會(huì)切換到另外一種狀態(tài), 也可能會(huì)保持當(dāng)前狀態(tài)不變。 這些數(shù)量有限且預(yù)先定義的狀態(tài)切換規(guī)則被稱為轉(zhuǎn)移。
你還可將該方法應(yīng)用在對(duì)象上。 假如你有一個(gè) 文檔 Document類。 文檔可能會(huì)處于 草稿 Draft 、 審閱中 Moderation和 已發(fā)布 Published三種狀態(tài)中的一種。 文檔的 publish 發(fā)布方法在不同狀態(tài)下的行為略有不同:
- 處于
草稿狀態(tài)時(shí), 它會(huì)將文檔轉(zhuǎn)移到審閱中狀態(tài)。 - 處于
審閱中狀態(tài)時(shí), 如果當(dāng)前用戶是管理員, 它會(huì)公開發(fā)布文檔。 - 處于
已發(fā)布狀態(tài)時(shí), 它不會(huì)進(jìn)行任何操作。

狀態(tài)機(jī)通常由眾多條件運(yùn)算符 ( if 或 switch ) 實(shí)現(xiàn), 可根據(jù)對(duì)象的當(dāng)前狀態(tài)選擇相應(yīng)的行為。 “狀態(tài)” 通常只是對(duì)象中的一組成員變量值。 即使你之前從未聽說過有限狀態(tài)機(jī), 你也很可能已經(jīng)實(shí)現(xiàn)過狀態(tài)模式。 下面的代碼應(yīng)該能幫助你回憶起來。
class Document is
field state: string
// ……
method publish() is
switch (state)
"draft":
state = "moderation"
break
"moderation":
if (currentUser.role == "admin")
state = "published"
break
"published":
// 什么也不做。
break
// ……當(dāng)我們逐步在 文檔 類中添加更多狀態(tài)和依賴于狀態(tài)的行為后, 基于條件語句的狀態(tài)機(jī)就會(huì)暴露其最大的弱點(diǎn)。 為了能根據(jù)當(dāng)前狀態(tài)選擇完成相應(yīng)行為的方法, 絕大部分方法中會(huì)包含復(fù)雜的條件語句。 修改其轉(zhuǎn)換邏輯可能會(huì)涉及到修改所有方法中的狀態(tài)條件語句, 導(dǎo)致代碼的維護(hù)工作非常艱難。
這個(gè)問題會(huì)隨著項(xiàng)目進(jìn)行變得越發(fā)嚴(yán)重。 我們很難在設(shè)計(jì)階段預(yù)測(cè)到所有可能的狀態(tài)和轉(zhuǎn)換。 隨著時(shí)間推移, 最初僅包含有限條件語句的簡(jiǎn)潔狀態(tài)機(jī)可能會(huì)變成臃腫的一團(tuán)亂麻。
解決方案
狀態(tài)模式建議為對(duì)象的所有可能狀態(tài)新建一個(gè)類, 然后將所有狀態(tài)的對(duì)應(yīng)行為抽取到這些類中。
原始對(duì)象被稱為上下文 (context), 它并不會(huì)自行實(shí)現(xiàn)所有行為, 而是會(huì)保存一個(gè)指向表示當(dāng)前狀態(tài)的狀態(tài)對(duì)象的引用, 且將所有與狀態(tài)相關(guān)的工作委派給該對(duì)象。

如需將上下文轉(zhuǎn)換為另外一種狀態(tài), 則需將當(dāng)前活動(dòng)的狀態(tài)對(duì)象替換為另外一個(gè)代表新狀態(tài)的對(duì)象。 采用這種方式是有前提的: 所有狀態(tài)類都必須遵循同樣的接口, 而且上下文必須僅通過接口與這些對(duì)象進(jìn)行交互。
這個(gè)結(jié)構(gòu)可能看上去與策略模式相似, 但有一個(gè)關(guān)鍵性的不同——在狀態(tài)模式中, 特定狀態(tài)知道其他所有狀態(tài)的存在, 且能觸發(fā)從一個(gè)狀態(tài)到另一個(gè)狀態(tài)的轉(zhuǎn)換; 策略則幾乎完全不知道其他策略的存在。
真實(shí)世界類比
智能手機(jī)的按鍵和開關(guān)會(huì)根據(jù)設(shè)備當(dāng)前狀態(tài)完成不同行為:
- 當(dāng)手機(jī)處于解鎖狀態(tài)時(shí), 按下按鍵將執(zhí)行各種功能。
- 當(dāng)手機(jī)處于鎖定狀態(tài)時(shí), 按下任何按鍵都將解鎖屏幕。
- 當(dāng)手機(jī)電量不足時(shí), 按下任何按鍵都將顯示充電頁面。
狀態(tài)模式結(jié)構(gòu)

上下文 (Context) 保存了對(duì)于一個(gè)具體狀態(tài)對(duì)象的引用, 并會(huì)將所有與該狀態(tài)相關(guān)的工作委派給它。 上下文通過狀態(tài)接口與狀態(tài)對(duì)象交互, 且會(huì)提供一個(gè)設(shè)置器用于傳遞新的狀態(tài)對(duì)象。
狀態(tài) (State) 接口會(huì)聲明特定于狀態(tài)的方法。 這些方法應(yīng)能被其他所有具體狀態(tài)所理解, 因?yàn)槟悴幌M承顟B(tài)所擁有的方法永遠(yuǎn)不會(huì)被調(diào)用。
具體狀態(tài) (Concrete States) 會(huì)自行實(shí)現(xiàn)特定于狀態(tài)的方法。 為了避免多個(gè)狀態(tài)中包含相似代碼, 你可以提供一個(gè)封裝有部分通用行為的中間抽象類。
狀態(tài)對(duì)象可存儲(chǔ)對(duì)于上下文對(duì)象的反向引用。 狀態(tài)可以通過該引用從上下文處獲取所需信息, 并且能觸發(fā)狀態(tài)轉(zhuǎn)移。
上下文和具體狀態(tài)都可以設(shè)置上下文的下個(gè)狀態(tài), 并可通過替換連接到上下文的狀態(tài)對(duì)象來完成實(shí)際的狀態(tài)轉(zhuǎn)換。
偽代碼
在本例中, 狀態(tài)模式將根據(jù)當(dāng)前回放狀態(tài), 讓媒體播放器中的相同控件完成不同的行為。

使用狀態(tài)對(duì)象更改對(duì)象行為的示例。
播放器的主要對(duì)象總是會(huì)連接到一個(gè)負(fù)責(zé)播放器絕大部分工作的狀態(tài)對(duì)象中。 部分操作會(huì)更換播放器當(dāng)前的狀態(tài)對(duì)象, 以此改變播放器對(duì)于用戶互動(dòng)所作出的反應(yīng)。
// 音頻播放器(Audio-Player)類即為上下文。它還會(huì)維護(hù)指向狀態(tài)類實(shí)例的引用,
// 該狀態(tài)類則用于表示音頻播放器當(dāng)前的狀態(tài)。
class AudioPlayer is
field state: State
field UI, volume, playlist, currentSong
constructor AudioPlayer() is
this.state = new ReadyState(this)
// 上下文會(huì)將處理用戶輸入的工作委派給狀態(tài)對(duì)象。由于每個(gè)狀態(tài)都以不
// 同的方式處理輸入,其結(jié)果自然將依賴于當(dāng)前所處的狀態(tài)。
UI = new UserInterface()
UI.lockButton.onClick(this.clickLock)
UI.playButton.onClick(this.clickPlay)
UI.nextButton.onClick(this.clickNext)
UI.prevButton.onClick(this.clickPrevious)
// 其他對(duì)象必須能切換音頻播放器當(dāng)前所處的狀態(tài)。
method changeState(state: State) is
this.state = state
// UI 方法會(huì)將執(zhí)行工作委派給當(dāng)前狀態(tài)。
method clickLock() is
state.clickLock()
method clickPlay() is
state.clickPlay()
method clickNext() is
state.clickNext()
method clickPrevious() is
state.clickPrevious()
// 狀態(tài)可調(diào)用上下文的一些服務(wù)方法。
method startPlayback() is
// ……
method stopPlayback() is
// ……
method nextSong() is
// ……
method previousSong() is
// ……
method fastForward(time) is
// ……
method rewind(time) is
// ……
// 所有具體狀態(tài)類都必須實(shí)現(xiàn)狀態(tài)基類聲明的方法,并提供反向引用指向與狀態(tài)相
// 關(guān)的上下文對(duì)象。狀態(tài)可使用反向引用將上下文轉(zhuǎn)換為另一個(gè)狀態(tài)。
abstract class State is
protected field player: AudioPlayer
// 上下文將自身傳遞給狀態(tài)構(gòu)造函數(shù)。這可幫助狀態(tài)在需要時(shí)獲取一些有用的
// 上下文數(shù)據(jù)。
constructor State(player) is
this.player = player
abstract method clickLock()
abstract method clickPlay()
abstract method clickNext()
abstract method clickPrevious()
// 具體狀態(tài)會(huì)實(shí)現(xiàn)與上下文狀態(tài)相關(guān)的多種行為。
class LockedState extends State is
// 當(dāng)你解鎖一個(gè)鎖定的播放器時(shí),它可能處于兩種狀態(tài)之一。
method clickLock() is
if (player.playing)
player.changeState(new PlayingState(player))
else
player.changeState(new ReadyState(player))
method clickPlay() is
// 已鎖定,什么也不做。
method clickNext() is
// 已鎖定,什么也不做。
method clickPrevious() is
// 已鎖定,什么也不做。
// 它們還可在上下文中觸發(fā)狀態(tài)轉(zhuǎn)換。
class ReadyState extends State 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 extends State 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)狀態(tài)模式適合應(yīng)用場(chǎng)景
- 如果對(duì)象需要根據(jù)自身當(dāng)前狀態(tài)進(jìn)行不同行為, 同時(shí)狀態(tài)的數(shù)量非常多且與狀態(tài)相關(guān)的代碼會(huì)頻繁變更的話, 可使用狀態(tài)模式。
模式建議你將所有特定于狀態(tài)的代碼抽取到一組獨(dú)立的類中。 這樣一來, 你可以在獨(dú)立于其他狀態(tài)的情況下添加新狀態(tài)或修改已有狀態(tài), 從而減少維護(hù)成本。
- 如果某個(gè)類需要根據(jù)成員變量的當(dāng)前值改變自身行為, 從而需要使用大量的條件語句時(shí), 可使用該模式。
狀態(tài)模式會(huì)將這些條件語句的分支抽取到相應(yīng)狀態(tài)類的方法中。 同時(shí), 你還可以清除主要類中與特定狀態(tài)相關(guān)的臨時(shí)成員變量和幫手方法代碼。
- 當(dāng)相似狀態(tài)和基于條件的狀態(tài)機(jī)轉(zhuǎn)換中存在許多重復(fù)代碼時(shí), 可使用狀態(tài)模式。
狀態(tài)模式讓你能夠生成狀態(tài)類層次結(jié)構(gòu), 通過將公用代碼抽取到抽象基類中來減少重復(fù)。
實(shí)現(xiàn)方式
確定哪些類是上下文。 它可能是包含依賴于狀態(tài)的代碼的已有類; 如果特定于狀態(tài)的代碼分散在多個(gè)類中, 那么它可能是一個(gè)新的類。
聲明狀態(tài)接口。 雖然你可能會(huì)需要完全復(fù)制上下文中聲明的所有方法, 但最好是僅把關(guān)注點(diǎn)放在那些可能包含特定于狀態(tài)的行為的方法上。
為每個(gè)實(shí)際狀態(tài)創(chuàng)建一個(gè)繼承于狀態(tài)接口的類。 然后檢查上下文中的方法并將與特定狀態(tài)相關(guān)的所有代碼抽取到新建的類中。
在將代碼移動(dòng)到狀態(tài)類的過程中, 你可能會(huì)發(fā)現(xiàn)它依賴于上下文中的一些私有成員。 你可以采用以下幾種變通方式:
- 將這些成員變量或方法設(shè)為公有。
- 將需要抽取的上下文行為更改為上下文中的公有方法, 然后在狀態(tài)類中調(diào)用。 這種方式簡(jiǎn)陋卻便捷, 你可以稍后再對(duì)其進(jìn)行修補(bǔ)。
- 將狀態(tài)類嵌套在上下文類中。 這種方式需要你所使用的編程語言支持嵌套類。
在上下文類中添加一個(gè)狀態(tài)接口類型的引用成員變量, 以及一個(gè)用于修改該成員變量值的公有設(shè)置器。
再次檢查上下文中的方法, 將空的條件語句替換為相應(yīng)的狀態(tài)對(duì)象方法。
為切換上下文狀態(tài), 你需要?jiǎng)?chuàng)建某個(gè)狀態(tài)類實(shí)例并將其傳遞給上下文。 你可以在上下文、 各種狀態(tài)或客戶端中完成這項(xiàng)工作。 無論在何處完成這項(xiàng)工作, 該類都將依賴于其所實(shí)例化的具體類。
狀態(tài)模式優(yōu)缺點(diǎn)
單一職責(zé)原則。 將與特定狀態(tài)相關(guān)的代碼放在單獨(dú)的類中。
開閉原則。 無需修改已有狀態(tài)類和上下文就能引入新狀態(tài)。
通過消除臃腫的狀態(tài)機(jī)條件語句簡(jiǎn)化上下文代碼。
如果狀態(tài)機(jī)只有很少的幾個(gè)狀態(tài), 或者很少發(fā)生改變, 那么應(yīng)用該模式可能會(huì)顯得小題大作。
代碼示例
Go設(shè)計(jì)模式之狀態(tài)模式講解和代碼示例_Golang_腳本之家 (jb51.net)
以上就是Go設(shè)計(jì)模式之狀態(tài)模式圖文詳解的詳細(xì)內(nèi)容,更多關(guān)于Go狀態(tài)模式的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Go語言學(xué)習(xí)之結(jié)構(gòu)體和方法使用詳解
這篇文章主要為大家詳細(xì)介紹了Go語言中結(jié)構(gòu)體和方法的使用,文中的示例代碼講解詳細(xì),對(duì)我們學(xué)習(xí)Go語言有一定的幫助,需要的可以參考一下2022-04-04
一文總結(jié)Go語言切片核心知識(shí)點(diǎn)和坑
都說Go的切片用起來絲滑得很,Java中的List怎么用,切片就怎么用,作為曾經(jīng)的Java選手,因?yàn)榍衅氖褂貌坏卯?dāng),喜提缺陷若干,本文就給大家總結(jié)一下Go語言切片核心知識(shí)點(diǎn)和坑,需要的朋友可以參考下2023-06-06
Go中defer使用場(chǎng)景及注意事項(xiàng)
defer 會(huì)在當(dāng)前函數(shù)返回前執(zhí)行傳入的函數(shù),它會(huì)經(jīng)常被用于關(guān)閉文件描述符、關(guān)閉數(shù)據(jù)庫連接以及解鎖資源。這篇文章主要介紹了Go中defer使用注意事項(xiàng),需要的朋友可以參考下2021-12-12
并發(fā)安全本地化存儲(chǔ)go-cache讀寫鎖實(shí)現(xiàn)多協(xié)程并發(fā)訪問
這篇文章主要介紹了并發(fā)安全本地化存儲(chǔ)go-cache讀寫鎖實(shí)現(xiàn)多協(xié)程并發(fā)訪問,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-10-10

