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

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

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