設(shè)計模式--狀態(tài)模式

目錄

本文的結(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類圖如下:


20171129_state02.png

可以發(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ā)帖為例,簡單用代碼描述一下:

20171129_state01.png

假設(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)具有更好的靈活性和可擴展性。

20171129_state03.png

抽象狀態(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)下它所具有的行為。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。