狀態模式(Stats Pattern)

在實際項目中,應用程序往往需要根據不同的情況做出不同的處理。在開發工程中,需要考慮到各種場景、分支,常常會使用到if..else或者switch case等分支,通過判斷條件處理不同的情況。當這種判斷變得複雜的時候,分支增多,代碼量增多,對代碼的維護和可讀性、擴展性帶來了不好的影響。這種時候就可以考慮使用狀態模式。

狀態模式

允許一個對象在其內部狀態改變時改變它的行爲。對象看起來似乎修改了它的類。

在很多情況下,一個對象的行爲取決於一個或多個動態變化的屬性,這樣的屬性叫做狀態,這樣的對象叫做有狀態的(stateful)對象,這樣的對象狀態是從事先定義好的一系列值中取出的。當一個這樣的對象與外部事件產生互動時,其內部狀態就會改變,從而使得系統的行爲也隨之發生變化。


例子1:按鈕來控制一個電梯的狀態,一個電梯開們,關門,停,運行。每一種狀態改變,都有可能要根據其他狀態來更新處理。例如,開門狀體,你不能在運行的時候開門,而是在電梯定下後才能開門。


例子2:我們給一部手機打電話,就可能出現這幾種情況:用戶開機,用戶關機,用戶欠費停機,用戶消戶等。 所以當我們撥打這個號碼的時候:系統就要判斷,該用戶是否在開機且不忙狀態,又或者是關機,欠費等狀態。但不管是那種狀態我們都應給出對應的處理操作。


適用場景
1.一個對象的行爲取決於它的狀態,並且它必須在運行時刻根據狀態改變它的行爲。
2.一個操作中含有龐大的多分支結構,並且這些分支決定於對象的狀態。


角色說明

1.環境類(Context):  定義客戶感興趣的接口。維護一個ConcreteState子類的實例,這個實例定義當前狀態。
2.抽象狀態類(State):  定義一個接口以封裝與Context的一個特定狀態相關的行爲。
3.具體狀態類(ConcreteState):  每一子類實現一個與Context的一個狀態相關的行爲。


類圖



代碼示例

狀態模式最出名的莫過於狀態機了,這裏用代碼模擬一個娃娃機(Context)。簡單模擬一下娃娃機的狀態分別爲:空倉,等待投幣,操作中三個具體狀態(ConcreteStats);用戶對娃娃機的行爲包括:補貨,投幣,搖桿操作,抓娃娃操作。

先寫一段僞代碼,看看不用狀態模式時是怎麼寫的

switch (事件){
    case 投幣:
        if(等待狀態){
            //可以投幣
        }else if(操作中狀態){
            //不能投幣
        }else {
            //空倉狀態,不能投幣
        }
        return;
    case 搖桿:
        //…………
    case 抓娃娃:
        //…………
    case 補貨:
        //…………
}
爲了縮短代碼長度,寫得比較粗糙,不過大意就是:不同的事件需要判斷娃娃機當前的狀態,然後根據狀態做出相應的響應。如果這段代碼實實在在的都寫完寫好了,相比是一大段判斷語句,一大段代碼,可讀性、擴展性都不太好。


下面我們試着使用狀態模式完成娃娃機

Stats:

/**
 * 定義了4個娃娃機的行爲
 * Created by yangjiachang on 2016/9/12.
 */
public abstract class Stats {

    //娃娃機實例
    protected StatsMachine machine;
    /**
     * 投幣
     */
    public abstract void insertCoins();
    /**
     * 搖桿
     */
    public abstract void rock();
    /**
     * 抓娃娃
     */
    public abstract void get();
    /**
     * 補貨
     */
    public abstract void supplement(int count);
}

ConcreteStats:

/**
 * 空倉狀態,沒有娃娃了
 * Created by yangjiachang on 2016/9/12.
 */
public class EmptyStats extends Stats {

    public EmptyStats(StatsMachine machine){
        this.machine = machine;
    }

    public EmptyStats(){
    }

    public void insertCoins() {
        System.out.println("沒有娃娃了,不能投幣,錢退還給你");
    }

    public void rock() {
        System.out.println("搖桿無效");
    }

    public void get() {
        System.out.println("不能抓娃娃");
    }

    public void supplement(int count) {
        machine.setCount(machine.getCount() + count);
        machine.setStats(machine.getWaitStats());
        System.out.println("補貨完成,又有" + machine.getCount()+"個娃娃了");
    }
}
/**
 * 操作搖桿狀態,抓娃娃
 * Created by yangjiachang on 2016/9/12.
 */
public class OperateStats extends Stats {

    public OperateStats(StatsMachine machine){
        this.machine = machine;
    }
    public OperateStats(){
    }


    public void insertCoins() {
        System.out.println("已經投過錢幣了,不要再投了,多的錢退給你");
    }

    /**
     * 搖桿
     */
    public void rock() {
        System.out.println("尋找一個最好的娃娃");
    }

    /**
     * 抓娃娃
     */
    public void get() {
        System.out.println("正在抓取娃娃……");
        int count = machine.getCount()-1;
        if(count > 0){
            machine.setCount(count);
            machine.setStats(machine.getWaitStats());
        }else {
            machine.setCount(count);
            machine.setStats(machine.getEmptyStats());
        }
        System.out.println("抓到娃娃,剩餘:"+machine.getCount());

    }

    public void supplement(int count) {
        System.out.println("用戶正在操作,請稍後補貨");
    }
}
/**
 * 等待投幣狀態
 * Created by yangjiachang on 2016/9/12.
 */
public class WaitStats extends Stats {
    public WaitStats(StatsMachine machine){
        this.machine = machine;
    }

    public WaitStats(){}

    public void insertCoins() {
        System.out.println("投幣成功,你可以開始抓娃娃了");
        machine.setStats(machine.getOperateStats());
    }

    public void rock() {
        System.out.println("搖桿無效,請先投幣");
    }

    public void get() {
        System.out.println("不能抓娃娃,請先投幣");
    }

    public void supplement(int count) {
        machine.setCount(machine.getCount() + count);
        System.out.println("補貨完成,又有" + machine.getCount()+"個娃娃了");
    }
}
Context:

/**
 * 定義一個狀態機,這裏就是娃娃機
 * Created by yangjiachang on 2016/9/12.
 */
public class StatsMachine {
    //娃娃機存在的三個狀態
    private Stats emptyStats = new EmptyStats(this);
    private Stats waitStats = new WaitStats(this);
    private Stats operateStats = new OperateStats(this);
    //記錄當前狀態
    private Stats stats;
    //娃娃數量
    private int count;

    //初始化
    public StatsMachine(int count){
        if (count <= 0){
            this.count = 0;
            this.stats = emptyStats;
        }else {
            this.count = count;
            this.stats = waitStats;
        }
    }

    /* 行爲start */
    public void insertCoins(){
        stats.insertCoins();
    }
    public void rock(){
        stats.rock();
    }
    public void get(){
        stats.get();
    }
    public void supplement(int count){
        stats.supplement(count);
    }
    /* 行爲end */

    //setting  getting
    public Stats getStats() {
        return stats;
    }

    public void setStats(Stats stats) {
        this.stats = stats;
    }

    public Stats getEmptyStats() {
        return emptyStats;
    }

    public Stats getWaitStats() {
        return waitStats;
    }

    public Stats getOperateStats() {
        return operateStats;
    }

    public int getCount() {
        return count;
    }

    public void setCount(int count) {
        this.count = count;
    }
}
測試方法:

public static void main(String[] args) {
    int count = 2;
    StatsMachine machine = new StatsMachine(count);
    for(int i=0 ; i<count+1 ; i++){
        System.out.println("當前狀態:"+machine.getStats().getClass().getSimpleName());
        machine.insertCoins();
        System.out.println("當前狀態:" + machine.getStats().getClass().getSimpleName());
        machine.rock();
        System.out.println("當前狀態:" + machine.getStats().getClass().getSimpleName());
        machine.get();
        System.out.println();
    }
    machine.supplement(3);
    System.out.println("剩餘數量:" + machine.getCount());
}

執行結果:

當前狀態:WaitStats
投幣成功,你可以開始抓娃娃了
當前狀態:OperateStats
尋找一個最好的娃娃
當前狀態:OperateStats
正在抓取娃娃……
抓到娃娃,剩餘:1


當前狀態:WaitStats
投幣成功,你可以開始抓娃娃了
當前狀態:OperateStats
尋找一個最好的娃娃
當前狀態:OperateStats
正在抓取娃娃……
抓到娃娃,剩餘:0


當前狀態:EmptyStats
沒有娃娃了,不能投幣,錢退還給你
當前狀態:EmptyStats
搖桿無效
當前狀態:EmptyStats
不能抓娃娃


補貨完成,又有3個娃娃了
剩餘數量:3


對狀態模式的理解:

1.它將與特定狀態相關的行爲局部化,並且將不同狀態的行爲分割開來

將狀態封裝成對象,由抽象狀態類定義所有的行爲;將Context的每種狀態定義成一個具體狀態類,並實現該狀態下各種行爲的實現。所以通過定義新的子類很容易擴展新的狀態和轉移狀態(例如投幣行爲將娃娃機由等待投幣狀態變爲可操作狀態),並實現該新狀態下的行爲。這種方式避免了過多的if..else或者switch case語句帶來的分支代碼,並將各自狀態的邏輯封裝到一個具體狀態類中,對於理解該狀態下什麼能做、做了什麼提供了更好的可讀性。


2.顯示的轉換狀態,增強可讀性

實際應用中,狀態的判斷往往的多個變量的值的判斷,在換貨狀態時,需要對多個變量賦值,這樣的方式加深了理解的難道。通過狀態模式,將狀態封裝在一個類中,在判斷與轉換的過程中,更加直截了當


3.狀態可以共享

如果有必要,可以多個Context共享同一個Stats狀態,也可以減少


4.將狀態封裝成對象,勢必會增加系統類和對象個數,運用不當反而會顯得結構和代碼混亂








發佈了45 篇原創文章 · 獲贊 65 · 訪問量 9萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章