目錄
本文的結(jié)構(gòu)如下:
- 引言
- 什么是狀態(tài)模式
- 模式的結(jié)構(gòu)
- 典型代碼
- 代碼示例
- 狀態(tài)模式和策略模式的區(qū)別
- 優(yōu)點和缺點
- 適用環(huán)境
- 模式應(yīng)用
一、引言
要說現(xiàn)在的生活真的是蠻方便的,就拿現(xiàn)在的共享單車來說,下班時間,主要線路都堵爆了,以往只能龜行在車海中,現(xiàn)在只需找一輛單車騎回去,省時還健身。
但現(xiàn)在的共享單車基本上都需要先掃碼開鎖。如果找到一輛單車,但是卻壞掉了,會顯示等待維修之類的狀態(tài),掃不開,也騎不了;然后要換下一輛,剛好有一輛上面掛著塑料袋,旁邊還有人扶著,你過去掃碼,肯定是顯示已占用之類的狀態(tài),同樣是掃不開,騎不了;只有正常的單車才能掃開,然后讓你騎行。這里面有幾種狀態(tài),正常等待掃碼狀態(tài),損壞等待維修狀態(tài),正在騎行狀態(tài)......
在軟件開發(fā)中,同樣有這樣的情況,有些對象也具有多種狀態(tài),這些狀態(tài)在某些情況下能夠相互轉(zhuǎn)換,而且對象在不同的狀態(tài)下也將具有不同的行為。為了更好地對這些具有多種狀態(tài)的對象進行設(shè)計,可以使用一種被稱之為狀態(tài)模式的設(shè)計模式。
二、什么是狀態(tài)模式
狀態(tài)模式用于解決系統(tǒng)中復(fù)雜對象的狀態(tài)轉(zhuǎn)換以及不同狀態(tài)下行為的封裝問題。當(dāng)系統(tǒng)中某個對象存在多個狀態(tài),這些狀態(tài)之間可以進行轉(zhuǎn)換,而且對象在不同狀態(tài)下行為不相同時可以使用狀態(tài)模式。
狀態(tài)模式將一個對象的狀態(tài)從該對象中分離出來,封裝到專門的狀態(tài)類中,使得對象狀態(tài)可以靈活變化,對于客戶端而言,無須關(guān)心對象狀態(tài)的轉(zhuǎn)換以及對象所處的當(dāng)前狀態(tài),無論對于何種狀態(tài)的對象,客戶端都可以一致處理。
狀態(tài)模式定義如下:
狀態(tài)模式(State Pattern):允許一個對象在其內(nèi)部狀態(tài)改變時改變它的行為,對象看起來似乎修改了它的類。其別名為狀態(tài)對象(Objects for States),狀態(tài)模式是一種對象行為型模式。
“允許一個對象在其內(nèi)部狀態(tài)改變時改變它的行為”應(yīng)該比較好理解,“對象看起來似乎修改了它的類”表達不是很清楚,大概的意思是對象的狀態(tài)轉(zhuǎn)變對客戶端是不可知的,但卻表現(xiàn)出不同的行為,就像修改了類一樣。
三、模式的結(jié)構(gòu)
狀態(tài)模式的UML類圖如下:
可以發(fā)現(xiàn)狀態(tài)模式的類圖和策略模式是一模一樣的,那這兩個模式有什么區(qū)別,這個后面再說,先繼續(xù)看狀態(tài)模式的結(jié)構(gòu)。
在狀態(tài)模式結(jié)構(gòu)圖中包含如下幾個角色:
- Context(環(huán)境類):環(huán)境類又稱為上下文類,它是擁有多種狀態(tài)的對象。由于環(huán)境類的狀態(tài)存在多樣性且在不同狀態(tài)下對象的行為有所不同,因此將狀態(tài)獨立出去形成單獨的狀態(tài)類。在環(huán)境類中維護一個抽象狀態(tài)類State的實例,這個實例定義當(dāng)前狀態(tài),在具體實現(xiàn)時,它是一個State子類的對象。
- State(抽象狀態(tài)類):它用于定義一個接口以封裝與環(huán)境類的一個特定狀態(tài)相關(guān)的行為,在抽象狀態(tài)類中聲明了各種不同狀態(tài)對應(yīng)的方法,而在其子類中實現(xiàn)類這些方法,由于不同狀態(tài)下對象的行為可能不同,因此在不同子類中方法的實現(xiàn)可能存在不同,相同的方法可以寫在抽象狀態(tài)類中。
- ConcreteState(具體狀態(tài)類):它是抽象狀態(tài)類的子類,每一個子類實現(xiàn)一個與環(huán)境類的一個狀態(tài)相關(guān)的行為,每一個具體狀態(tài)類對應(yīng)環(huán)境的一個具體狀態(tài),不同的具體狀態(tài)類其行為有所不同。
四、典型代碼
狀態(tài)模式將對象在不同狀態(tài)下的行為封裝到不同的狀態(tài)類中,為了讓系統(tǒng)具有更好的靈活性和可擴展性,同時對各狀態(tài)下的共有行為進行封裝,需要對狀態(tài)進行抽象,引入了抽象狀態(tài)類角色,其典型代碼如下:
public abstract class State {
//不同的狀態(tài)有不同的實現(xiàn),也可是是不同的狀態(tài)有不同的方法
public abstract void handle();
}
不同的具體狀態(tài)類可以提供完全不同的方法實現(xiàn),也可能包含多個業(yè)務(wù)方法,如果需要,應(yīng)該將通用的方法放到抽象層,其典型代碼如下:
public class ConcreteStateA extends State {
public void handle() {
//todo
}
}
public class ConcreteStateB extends State {
public void handle() {
//todo
}
}
環(huán)境類中需維持一個對抽象狀態(tài)類的引用,可以通過調(diào)用setState()方法改變其狀態(tài)(即不同的狀態(tài)實現(xiàn)),再在環(huán)境類的業(yè)務(wù)方法中調(diào)用狀態(tài)對象的方法,典型代碼如下所示:
public class Context {
private State state;
private int value; //某個屬性值,對象狀態(tài)發(fā)生變化由該值引發(fā)
public void setState(State state){
this.state = state;
}
public void request(){
this.state.handle();
}
}
環(huán)境類實際上是真正擁有狀態(tài)的對象,狀態(tài)模式遵循“封裝變化”的指導(dǎo)思想,將環(huán)境類中與狀態(tài)有關(guān)的代碼提取出來封裝到專門的狀態(tài)類中,之所以說是變化的,因為一個對象的狀態(tài)之間可以進行相互轉(zhuǎn)換。
這種轉(zhuǎn)換是通過改變state的具體指向達到的,多是通過暴露的公有setState()方法實現(xiàn),一般有兩種方式,一種是在Context類中,根據(jù)value值的變化,改變狀態(tài);另一種就是在具體狀態(tài)類中根據(jù)value值變化,改變狀態(tài),Context應(yīng)該提供一個getValue()的方法。
五、代碼示例
當(dāng)今社會,論壇貼吧很多,我們也會加入感興趣的論壇,偶爾進行發(fā)言,但有時卻會發(fā)現(xiàn)不能發(fā)帖了,原來是昨天的某個帖子引發(fā)了口水戰(zhàn),被舉報了。這里就用論壇發(fā)帖為例,簡單用代碼描述一下:
假設(shè)有三種狀態(tài),normal(正常),restricted(受限),closed(封號),判斷依據(jù)是一個健康值(這里只是假設(shè))。
5.1、不用狀態(tài)模式
public class Account {
public static final int NORMAL = 1;
public static final int RESTRICTED = 2;
public static final int CLOSED = 3;
private int healthValue;
private int state;
/**
* 看帖
*/
public void view(){
System.out.println("正常看帖");
//todo healthValue改變算法
changeState();
}
/**
* 評論
*/
public void comment(){
if (state == NORMAL || state == RESTRICTED){
System.out.println("正常評論");
//todo healthValue改變算法
changeState();
}else if (state == CLOSED){
System.out.println("抱歉,你的健康值小于-10,不能評論");
}
}
/**
* 發(fā)帖
*/
public void post(){
if (state == NORMAL){
//todo 一些health值改變算法
changeState();
System.out.println("正常發(fā)帖");
}else if (state == RESTRICTED || state == CLOSED){
System.out.println("抱歉,你的健康值小于0,不能發(fā)帖");
}
}
public void changeState(){
if (healthValue <= -10){
state = CLOSED;
}else if (-10 < healthValue && healthValue<= 0){
state = RESTRICTED;
}else if (healthValue > 0){
state = CLOSED;
}
}
public int getHealthValue() {
return healthValue;
}
public void setState(int state) {
this.state = state;
}
}
上面的代碼很簡單,能夠?qū)崿F(xiàn)需要的功能,但是卻有幾個問題:
- 看帖和發(fā)帖方法中都包含狀態(tài)判斷語句,以判斷在該狀態(tài)下是否具有該方法以及在特定狀態(tài)下該方法如何實現(xiàn),導(dǎo)致代碼非常冗長,可維護性較差;
- 擁有一個較為復(fù)雜的changeState()方法,包含大量的if...else...語句用于進行狀態(tài)轉(zhuǎn)換,代碼測試難度較大,且不易于維護;
- 系統(tǒng)擴展性較差,如果需要增加一種新的狀態(tài),如hot狀態(tài)(活躍用戶,該狀態(tài)用戶發(fā)帖積分增加更多),需要對原有代碼進行大量修改,擴展起來非常麻煩。
5.2、使用狀態(tài)模式
狀態(tài)模式可以在一定程度上解決上述問題,在狀態(tài)模式中將對象在每一個狀態(tài)下的行為和狀態(tài)轉(zhuǎn)移語句封裝在一個個狀態(tài)類中,通過這些狀態(tài)類來分散冗長的條件轉(zhuǎn)移語句,讓系統(tǒng)具有更好的靈活性和可擴展性。
抽象狀態(tài)和具體狀態(tài)類:
public abstract class State {
protected Account account;
public void view(){
System.out.println("正常看帖");
//todo healthValue改變算法
changeState();
}
public abstract void comment();
public abstract void post();
public void changeState(){
if (account.getHealthValue() <= -10){
account.setState(account.getClosedState());
}else if (account.getHealthValue() > -10 && account.getHealthValue() <= 0){
account.setState(account.getRestrictedState());
}else if (account.getHealthValue()>0){
account.setState(account.getNormalState());
}
}
}
public class NormalState extends State{
public NormalState(Account account){
this.account = account;
}
public void comment(){
System.out.println("正常評論");
//todo healthValue改變算法
changeState();
}
public void post() {
System.out.println("正常發(fā)帖");
//todo healthValue改變算法
changeState();
}
}
public class RestrictedState extends State {
public RestrictedState(Account account){
this.account = account;
}
public void comment(){
System.out.println("正常評論");
//todo healthValue改變算法
changeState();
}
public void post() {
System.out.println("抱歉,你的健康值小于0,不能發(fā)帖");
}
}
public class ClosedState extends State {
public ClosedState(Account account){
this.account = account;
}
public void comment(){
System.out.println("抱歉,你的健康值小于-10,不能評論");
}
public void post() {
System.out.println("抱歉,你的健康值小于-10,不能發(fā)帖");
}
public void changeState() {
if (account.getHealthValue() <= -10){
account.setState(account.getClosedState());
}else if (account.getHealthValue() > -10 && account.getHealthValue() <= 0){
account.setState(account.getRestrictedState());
}
}
}
環(huán)境類(賬戶):
public class Account {
private State normalState, restrictedState, closedState;
private String name;
private int healthValue;
private State state;
public Account(String name){
normalState = new NormalState(this);
restrictedState = new RestrictedState(this);
closedState = new ClosedState(this);
this.healthValue = 1;//新賬號默認(rèn)為1
this.state = normalState;
this.name = name;
}
/**
* 看帖
*/
public void view(){
System.out.println(name + "正在看帖");
state.view();
System.out.println("當(dāng)前賬戶狀態(tài)為:" + state.getClass().getName());
}
/**
* 評論
*/
public void comment(){
System.out.println(name + "正在評論");
state.comment();
System.out.println("當(dāng)前賬戶狀態(tài)為:" + state.getClass().getName());
}
/**
* 發(fā)帖
*/
public void post(){
System.out.println(name + "正在發(fā)帖");
state.post();
System.out.println("當(dāng)前賬戶狀態(tài)為:" + state.getClass().getName());
}
public int getHealthValue() {
return healthValue;
}
public void setState(State state) {
this.state = state;
}
public State getNormalState() {
return normalState;
}
public State getRestrictedState() {
return restrictedState;
}
public State getClosedState() {
return closedState;
}
}
六、狀態(tài)模式和策略模式的區(qū)別
前面說了,它們的UML類圖一模一樣,但卻是兩種設(shè)計模式,這里面肯定是有原因的。
- 策略(如促銷一種商品的策略)和狀態(tài)(如同一個按鈕來控制一個電梯的狀態(tài),又如手機界面中一個按鈕來控制手機)是兩種完全不同的思想。對狀態(tài)模式而言,狀態(tài)的轉(zhuǎn)換是一個核心內(nèi)容,而且這種轉(zhuǎn)換是對客戶端不可見的;然而于選擇策略,轉(zhuǎn)換與此毫無關(guān)系,它允許一個客戶選擇或提供一種策略。
- 策略是一組方案,可以相互替換,策略模式用于隨不同外部環(huán)境而主動采取不同行為的場合。
- 狀態(tài)模式處理的核心問題是狀態(tài)的轉(zhuǎn)換,把各個狀態(tài)和相應(yīng)的實現(xiàn)步驟封裝成一組簡單的繼承自一個接口或抽象類的類,通過另外的一個Context來操作他們之間的自動狀態(tài)轉(zhuǎn)換,通過event來自動實現(xiàn)各個狀態(tài)之間的跳轉(zhuǎn)。在整個生命周期中存在一個狀態(tài)的轉(zhuǎn)換曲線,這個轉(zhuǎn)換曲線對客戶是透明的。
- 在狀態(tài)模式中,狀態(tài)的轉(zhuǎn)換是由對象的內(nèi)部條件決定,外界只需關(guān)心其接口,不必關(guān)心其狀態(tài)對象的創(chuàng)建和轉(zhuǎn)化;而策略模式里,采取何種策略由外部條件(C)決定。
七、優(yōu)點和缺點
7.1、優(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è)務(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ù)。
7.2、缺點
狀態(tài)模式的主要缺點如下:
- 狀態(tài)模式的使用必然會增加系統(tǒng)中類和對象的個數(shù),導(dǎo)致系統(tǒng)運行開銷增大。
- 狀態(tài)模式的結(jié)構(gòu)與實現(xiàn)都較為復(fù)雜,如果使用不當(dāng)將導(dǎo)致程序結(jié)構(gòu)和代碼的混亂,增加系統(tǒng)設(shè)計的難度。
- 狀態(tài)模式對“開閉原則”的支持并不太好,增加新的狀態(tài)類需要修改那些負責(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)致代碼的可維護性和靈活性變差,不能方便地增加和刪除狀態(tài),使客戶類與類庫之間的耦合增強。
九、模式應(yīng)用
狀態(tài)模式在工作流或游戲等類型的軟件中得以廣泛使用,甚至可以用于這些系統(tǒng)的核心功能設(shè)計,如在政府OA辦公系統(tǒng)中,一個批文的狀態(tài)有多種:尚未辦理;正在辦理;正在批示;正在審核;已經(jīng)完成等各種狀態(tài),而且批文狀態(tài)不同時對批文的操作也有所差異。使用狀態(tài)模式可以描述工作流對象(如批文)的狀態(tài)轉(zhuǎn)換以及不同狀態(tài)下它所具有的行為。