狀態(tài)模式及其結(jié)構
狀態(tài)模式(State):當一個對象的內(nèi)部狀態(tài)發(fā)生改變時,會導致其行為的改變,對象看起來似乎修改了它的類。其別名為狀態(tài)對象(Objects for States),狀態(tài)模式是一種對象行為型模式。狀態(tài)模式用于解決系統(tǒng)中復雜對象的狀態(tài)轉(zhuǎn)換以及不同狀態(tài)下行為的封裝問題。當系統(tǒng)中某個對象存在多個狀態(tài),這些狀態(tài)之間可以進行轉(zhuǎn)換,而且對象在不同狀態(tài)下行為不相同時可以使用狀態(tài)模式。
模式的結(jié)構
UML

在狀態(tài)模式結(jié)構圖中包含如下幾個角色:
- Context(環(huán)境類):環(huán)境類又稱為上下文類,它是擁有多種狀態(tài)的對象。由于環(huán)境類的狀態(tài)存在多樣性且在不同狀態(tài)下對象的行為有所不同,因此將狀態(tài)獨立出去形成單獨的狀態(tài)類。在環(huán)境類中維護一個抽象狀態(tài)類State的實例,這個實例定義當前狀態(tài),在具體實現(xiàn)時,它是一個State子類的對象。
- State(抽象狀態(tài)類):它用于定義一個接口以封裝與環(huán)境類的一個特定狀態(tài)相關的行為,在抽象狀態(tài)類中聲明了各種不同狀態(tài)對應的方法,而在其子類中實現(xiàn)類這些方法,由于不同狀態(tài)下對象的行為可能不同,因此在不同子類中方法的實現(xiàn)可能存在不同,相同的方法可以寫在抽象狀態(tài)類中。
- ConcreteState(具體狀態(tài)類):它是抽象狀態(tài)類的子類,每一個子類實現(xiàn)一個與環(huán)境類的一個狀態(tài)相關的行為,每一個具體狀態(tài)類對應環(huán)境的一個具體狀態(tài),不同的具體狀態(tài)類其行為有所不同。
代碼示例
當今社會,論壇貼吧很多,我們也會加入感興趣的論壇,偶爾進行發(fā)言,但有時卻會發(fā)現(xiàn)不能發(fā)帖了,原來是昨天的某個帖子引發(fā)了口水戰(zhàn),被舉報了。這里就用論壇發(fā)帖為例,簡單用代碼描述一下:

假設有三種狀態(tài),normal(正常),restricted(受限),closed(封號),判斷依據(jù)是一個健康值(這里只是假設)。
2.1不用狀態(tài)模式
/* @Time : 2018/8/10 下午3:16
**@Author : panda
**@Email : codepanda_li@163.com
**@File : account.go
**@Software: GoLand
*/
package account
import "fmt"
type AccountState int
const (
NORMAL AccountState = iota //正常0
RESTRICTED //受限
CLOSED //封號
)
type Account struct {
State AccountState
HealthValue int
}
func NewAccount(health int) *Account {
a := &Account{
HealthValue: health,
}
a.changeState()
return a
}
///看帖
func (a *Account) View() {
if a.State == NORMAL || a.State == RESTRICTED {
fmt.Println("正常看帖")
} else if a.State == CLOSED {
fmt.Println("賬號被封,無法看帖")
}
}
///評論
func (a *Account) Comment() {
if a.State == NORMAL || a.State == RESTRICTED {
fmt.Println("正常評論")
} else if a.State == CLOSED {
fmt.Println("抱歉,你的健康值小于-10,不能評論")
}
}
///發(fā)帖
func (a *Account) Post() {
if a.State == NORMAL {
fmt.Println("正常發(fā)帖")
} else if a.State == RESTRICTED || a.State == CLOSED {
fmt.Println("抱歉,你的健康值小于0,不能發(fā)帖")
}
}
func (a *Account) changeState() {
if a.HealthValue <= -10 {
a.State = CLOSED
} else if a.HealthValue > -10 && a.HealthValue <= 0 {
a.State = RESTRICTED
} else if a.HealthValue > 0 {
a.State = NORMAL
}
}
///給賬戶設定健康值
func (a *Account) SetHealth(value int) {
a.HealthValue = value
a.changeState()
}
上面的代碼很簡單,能夠?qū)崿F(xiàn)需要的功能,但是卻有幾個問題:
- 看帖和發(fā)帖方法中都包含狀態(tài)判斷語句,以判斷在該狀態(tài)下是否具有該方法以及在特定狀態(tài)下該方法如何實現(xiàn),導致代碼非常冗長,可維護性較差;
- 系統(tǒng)擴展性較差,如果需要增加一種新的狀態(tài),如hot狀態(tài)(活躍用戶,該狀態(tài)用戶發(fā)帖積分增加更多),需要對原有代碼進行大量修改,擴展起來非常麻煩。
2.2使用狀態(tài)模式
狀態(tài)模式可以在一定程度上解決上述問題,在狀態(tài)模式中將對象在每一個狀態(tài)下的行為和狀態(tài)轉(zhuǎn)移語句封裝在一個個狀態(tài)類中,通過這些狀態(tài)類來分散冗長的條件轉(zhuǎn)移語句,讓系統(tǒng)具有更好的靈活性和可擴展性。
/* @Time : 2018/8/10 下午3:37
**@Author : panda
**@Email : codepanda_li@163.com
**@File : account.go
**@Software: GoLand
*/
package saccount
import "fmt"
type Account struct {
State ActionState
HealthValue int
}
func NewAccount(health int) *Account {
a := &Account{
HealthValue: health,
}
a.changeState()
return a
}
func (a *Account)View() {
a.State.View()
}
func (a *Account)Comment() {
a.State.Comment()
}
func (a *Account)Post() {
a.State.Post()
}
type ActionState interface {
View()
Comment()
Post()
}
type CloseState struct {
}
func (c *CloseState)View() {
fmt.Println("賬號被封,無法看帖")
}
func (c *CloseState)Comment() {
fmt.Println("抱歉,你的健康值小于-10,不能評論")
}
func (c *CloseState)Post() {
fmt.Println("抱歉,你的健康值小于0,不能發(fā)帖")
}
type RestrictedState struct {
}
func (r *RestrictedState)View() {
fmt.Println("正常看帖")
}
func (r *RestrictedState)Comment() {
fmt.Println("正常評論")
}
func (r *RestrictedState)Post() {
fmt.Println("抱歉,你的健康值小于0,不能發(fā)帖")
}
type NormalState struct {
}
func (n *NormalState)View() {
fmt.Println("正常看帖")
}
func (n *NormalState)Comment() {
fmt.Println("正常評論")
}
func (n *NormalState)Post() {
fmt.Println("正常發(fā)帖")
}
func (a *Account) changeState() {
if a.HealthValue <= -10 {
a.State = &CloseState{}
} else if a.HealthValue > -10 && a.HealthValue <= 0 {
a.State = &RestrictedState{}
} else if a.HealthValue > 0 {
a.State = &NormalState{}
}
}
///給賬戶設定健康值
func (a *Account) SetHealth(value int) {
a.HealthValue = value
a.changeState()
}
優(yōu)點和缺點
優(yōu)點
狀態(tài)模式的主要優(yōu)點如下:
- 封裝了狀態(tài)的轉(zhuǎn)換規(guī)則,在狀態(tài)模式中可以將狀態(tài)的轉(zhuǎn)換代碼封裝在環(huán)境類或者具體狀態(tài)類中,可以對狀態(tài)轉(zhuǎn)換代碼進行集中管理,而不是分散在一個個業(yè)務方法中。
- 將所有與某個狀態(tài)有關的行為放到一個類中,只需要注入一個不同的狀態(tài)對象即可使環(huán)境對象擁有不同的行為。
- 允許狀態(tài)轉(zhuǎn)換邏輯與狀態(tài)對象合成一體,而不是提供一個巨大的條件語句塊,狀態(tài)模式可以避免使用龐大的條件語句來將業(yè)務方法和狀態(tài)轉(zhuǎn)換代碼交織在一起。
- 可以讓多個環(huán)境對象共享一個狀態(tài)對象,從而減少系統(tǒng)中對象的個數(shù)。
缺點
狀態(tài)模式的主要缺點如下:
- 狀態(tài)模式的使用必然會增加系統(tǒng)中類和對象的個數(shù),導致系統(tǒng)運行開銷增大。
- 狀態(tài)模式的結(jié)構與實現(xiàn)都較為復雜,如果使用不當將導致程序結(jié)構和代碼的混亂,增加系統(tǒng)設計的難度。
- 狀態(tài)模式對“開閉原則”的支持并不太好,增加新的狀態(tài)類需要修改那些負責狀態(tài)轉(zhuǎn)換的源代碼,否則無法轉(zhuǎn)換到新增狀態(tài);而且修改某個狀態(tài)類的行為也需修改對應類的源代碼。
適用環(huán)境
在以下情況下可以使用狀態(tài)模式:
- 對象的行為依賴于它的狀態(tài)(屬性)并且可以根據(jù)它的狀態(tài)改變而改變它的相關行為。
- 代碼中包含大量與對象狀態(tài)有關的條件語句,這些條件語句的出現(xiàn),會導致代碼的可維護性和靈活性變差,不能方便地增加和刪除狀態(tài),使客戶類與類庫之間的耦合增強。
模式應用
狀態(tài)模式在工作流或游戲等類型的軟件中得以廣泛使用,甚至可以用于這些系統(tǒng)的核心功能設計,如在政府OA辦公系統(tǒng)中,一個批文的狀態(tài)有多種:尚未辦理;正在辦理;正在批示;正在審核;已經(jīng)完成等各種狀態(tài),而且批文狀態(tài)不同時對批文的操作也有所差異。使用狀態(tài)模式可以描述工作流對象(如批文)的狀態(tài)轉(zhuǎn)換以及不同狀態(tài)下它所具有的行為。
說明一下,這個貼子的示例是我印象中看過一個java的對狀態(tài)模式的實現(xiàn),覺得很恰當明了,然后自己用golang實現(xiàn)了一遍,現(xiàn)在只有goalng示例代碼,忘記了那篇java的出處了。對那個java的作者表示敬意。