狀態(tài)模式及其結(jié)構(gòu)
狀態(tài)模式(State):當(dāng)一個(gè)對(duì)象的內(nèi)部狀態(tài)發(fā)生改變時(shí)阀蒂,會(huì)導(dǎo)致其行為的改變何鸡,對(duì)象看起來似乎修改了它的類肾扰。其別名為狀態(tài)對(duì)象(Objects for States),狀態(tài)模式是一種對(duì)象行為型模式抄伍。狀態(tài)模式用于解決系統(tǒng)中復(fù)雜對(duì)象的狀態(tài)轉(zhuǎn)換以及不同狀態(tài)下行為的封裝問題熏挎。當(dāng)系統(tǒng)中某個(gè)對(duì)象存在多個(gè)狀態(tài),這些狀態(tài)之間可以進(jìn)行轉(zhuǎn)換前痘,而且對(duì)象在不同狀態(tài)下行為不相同時(shí)可以使用狀態(tài)模式凛捏。
模式的結(jié)構(gòu)
UML
在狀態(tài)模式結(jié)構(gòu)圖中包含如下幾個(gè)角色:
- Context(環(huán)境類):環(huán)境類又稱為上下文類,它是擁有多種狀態(tài)的對(duì)象芹缔。由于環(huán)境類的狀態(tài)存在多樣性且在不同狀態(tài)下對(duì)象的行為有所不同坯癣,因此將狀態(tài)獨(dú)立出去形成單獨(dú)的狀態(tài)類。在環(huán)境類中維護(hù)一個(gè)抽象狀態(tài)類State的實(shí)例最欠,這個(gè)實(shí)例定義當(dāng)前狀態(tài)示罗,在具體實(shí)現(xiàn)時(shí),它是一個(gè)State子類的對(duì)象芝硬。
- State(抽象狀態(tài)類):它用于定義一個(gè)接口以封裝與環(huán)境類的一個(gè)特定狀態(tài)相關(guān)的行為蚜点,在抽象狀態(tài)類中聲明了各種不同狀態(tài)對(duì)應(yīng)的方法,而在其子類中實(shí)現(xiàn)類這些方法拌阴,由于不同狀態(tài)下對(duì)象的行為可能不同绍绘,因此在不同子類中方法的實(shí)現(xiàn)可能存在不同,相同的方法可以寫在抽象狀態(tài)類中迟赃。
- ConcreteState(具體狀態(tài)類):它是抽象狀態(tài)類的子類陪拘,每一個(gè)子類實(shí)現(xiàn)一個(gè)與環(huán)境類的一個(gè)狀態(tài)相關(guān)的行為,每一個(gè)具體狀態(tài)類對(duì)應(yīng)環(huán)境的一個(gè)具體狀態(tài)纤壁,不同的具體狀態(tài)類其行為有所不同左刽。
代碼示例
當(dāng)今社會(huì),論壇貼吧很多酌媒,我們也會(huì)加入感興趣的論壇欠痴,偶爾進(jìn)行發(fā)言,但有時(shí)卻會(huì)發(fā)現(xiàn)不能發(fā)帖了馍佑,原來是昨天的某個(gè)帖子引發(fā)了口水戰(zhàn)斋否,被舉報(bào)了。這里就用論壇發(fā)帖為例拭荤,簡(jiǎn)單用代碼描述一下:
假設(shè)有三種狀態(tài)茵臭,normal(正常),restricted(受限)舅世,closed(封號(hào))旦委,判斷依據(jù)是一個(gè)健康值(這里只是假設(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 //封號(hào)
)
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("賬號(hào)被封缨硝,無法看帖")
}
}
///評(píng)論
func (a *Account) Comment() {
if a.State == NORMAL || a.State == RESTRICTED {
fmt.Println("正常評(píng)論")
} else if a.State == CLOSED {
fmt.Println("抱歉摩钙,你的健康值小于-10,不能評(píng)論")
}
}
///發(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()
}
上面的代碼很簡(jiǎn)單,能夠?qū)崿F(xiàn)需要的功能宜岛,但是卻有幾個(gè)問題:
- 看帖和發(fā)帖方法中都包含狀態(tài)判斷語(yǔ)句长踊,以判斷在該狀態(tài)下是否具有該方法以及在特定狀態(tài)下該方法如何實(shí)現(xiàn),導(dǎo)致代碼非常冗長(zhǎng)萍倡,可維護(hù)性較差身弊;
- 系統(tǒng)擴(kuò)展性較差,如果需要增加一種新的狀態(tài)列敲,如hot狀態(tài)(活躍用戶阱佛,該狀態(tài)用戶發(fā)帖積分增加更多),需要對(duì)原有代碼進(jìn)行大量修改戴而,擴(kuò)展起來非常麻煩凑术。
2.2使用狀態(tài)模式
狀態(tài)模式可以在一定程度上解決上述問題,在狀態(tài)模式中將對(duì)象在每一個(gè)狀態(tài)下的行為和狀態(tài)轉(zhuǎn)移語(yǔ)句封裝在一個(gè)個(gè)狀態(tài)類中填硕,通過這些狀態(tài)類來分散冗長(zhǎng)的條件轉(zhuǎn)移語(yǔ)句麦萤,讓系統(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("賬號(hào)被封扁眯,無法看帖")
}
func (c *CloseState)Comment() {
fmt.Println("抱歉,你的健康值小于-10翅帜,不能評(píng)論")
}
func (c *CloseState)Post() {
fmt.Println("抱歉姻檀,你的健康值小于0,不能發(fā)帖")
}
type RestrictedState struct {
}
func (r *RestrictedState)View() {
fmt.Println("正忱缘危看帖")
}
func (r *RestrictedState)Comment() {
fmt.Println("正常評(píng)論")
}
func (r *RestrictedState)Post() {
fmt.Println("抱歉绣版,你的健康值小于0,不能發(fā)帖")
}
type NormalState struct {
}
func (n *NormalState)View() {
fmt.Println("正臣叽看帖")
}
func (n *NormalState)Comment() {
fmt.Println("正常評(píng)論")
}
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)類中,可以對(duì)狀態(tài)轉(zhuǎn)換代碼進(jìn)行集中管理韩脏,而不是分散在一個(gè)個(gè)業(yè)務(wù)方法中缩麸。
- 將所有與某個(gè)狀態(tài)有關(guān)的行為放到一個(gè)類中,只需要注入一個(gè)不同的狀態(tài)對(duì)象即可使環(huán)境對(duì)象擁有不同的行為赡矢。
- 允許狀態(tài)轉(zhuǎn)換邏輯與狀態(tài)對(duì)象合成一體杭朱,而不是提供一個(gè)巨大的條件語(yǔ)句塊阅仔,狀態(tài)模式可以避免使用龐大的條件語(yǔ)句來將業(yè)務(wù)方法和狀態(tài)轉(zhuǎn)換代碼交織在一起。
- 可以讓多個(gè)環(huán)境對(duì)象共享一個(gè)狀態(tài)對(duì)象弧械,從而減少系統(tǒng)中對(duì)象的個(gè)數(shù)八酒。
缺點(diǎn)
狀態(tài)模式的主要缺點(diǎn)如下:
- 狀態(tài)模式的使用必然會(huì)增加系統(tǒng)中類和對(duì)象的個(gè)數(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)模式對(duì)“開閉原則”的支持并不太好衔瓮,增加新的狀態(tài)類需要修改那些負(fù)責(zé)狀態(tài)轉(zhuǎn)換的源代碼,否則無法轉(zhuǎn)換到新增狀態(tài)荒澡;而且修改某個(gè)狀態(tài)類的行為也需修改對(duì)應(yīng)類的源代碼报辱。
適用環(huán)境
在以下情況下可以使用狀態(tài)模式:
- 對(duì)象的行為依賴于它的狀態(tài)(屬性)并且可以根據(jù)它的狀態(tài)改變而改變它的相關(guān)行為。
- 代碼中包含大量與對(duì)象狀態(tài)有關(guān)的條件語(yǔ)句单山,這些條件語(yǔ)句的出現(xiàn)碍现,會(huì)導(dǎo)致代碼的可維護(hù)性和靈活性變差,不能方便地增加和刪除狀態(tài)米奸,使客戶類與類庫(kù)之間的耦合增強(qiáng)昼接。
模式應(yīng)用
狀態(tài)模式在工作流或游戲等類型的軟件中得以廣泛使用,甚至可以用于這些系統(tǒng)的核心功能設(shè)計(jì)悴晰,如在政府OA辦公系統(tǒng)中慢睡,一個(gè)批文的狀態(tài)有多種:尚未辦理;正在辦理铡溪;正在批示漂辐;正在審核;已經(jīng)完成等各種狀態(tài)棕硫,而且批文狀態(tài)不同時(shí)對(duì)批文的操作也有所差異髓涯。使用狀態(tài)模式可以描述工作流對(duì)象(如批文)的狀態(tài)轉(zhuǎn)換以及不同狀態(tài)下它所具有的行為。
說明一下哈扮,這個(gè)貼子的示例是我印象中看過一個(gè)java的對(duì)狀態(tài)模式的實(shí)現(xiàn)纬纪,覺得很恰當(dāng)明了,然后自己用golang實(shí)現(xiàn)了一遍滑肉,現(xiàn)在只有g(shù)oalng示例代碼包各,忘記了那篇java的出處了。對(duì)那個(gè)java的作者表示敬意靶庙。