狀態模式之策略模式的孿生兄弟

人的機緣是神奇的,認識一個人就相當於打開了一個圈子,不管這個人是否在圈子中心,而這點,會在不經意間帶給我們意想不到的作用。

如果我們在編寫代碼的時候,遇到大量的條件判斷的時候,可能會採用策略模式來優化結構,因爲這時涉及到策略的選擇,但有時候仔細查看下,就會發現,這些所謂的策略其實是對象的不同狀態,更加明顯的是,對象的某種狀態也成爲判斷的條件。

我們還是以一個例子入手。
假設現在我們有一個飲水機,它有以下兩個狀態: 滿桶,空桶。初始狀態是滿桶,容量是20。飲水機只有一個動作:press,每次press後都會使容量減1,一旦爲0,則將狀態設置爲空桶,這時press沒有水流出。

要使用狀態模式,我們必須明確兩個東西:狀態和每個狀態下執行的動作。就像是飲水機,最基本的狀態就是滿桶和空桶,而這兩個狀態下,都可能要執行倒水這個動作,也就是press。如果飲水機的容量爲0,則會進入空桶的狀態。

在狀態模式中,因爲所有的狀態都要執行相應的動作,所以我們可以考慮將狀態抽象出來。
狀態的抽象一般有兩種形式:接口和抽象類。如果所有的狀態都有共同的數據域,可以使用抽象類,但如果只是單純的執行動作,就可以使用接口。 這裏我們就用接口。

public interface DispenserState {
void press();
}

然後我們再定義滿桶和空桶兩個狀態:
複製代碼

public class FullState implements DispenserState {

@Override
public void press() {
    System.out.println("Water is pouring!");
}
}

public class NullState implements DispenserState {

@Override
public void press() {
    System.out.println("There is not water poured!");
}
}

複製代碼
接着我們再實現飲水機:
複製代碼

public class WaterDispenser {
private static int capacity = 20;
private static DispenserState dispenserState;

public WaterDispenser(DispenserState state) {
    dispenserState = state;
}

private static void setState(DispenserState state) {
    dispenserState = state;
}

public DispenserState getState() {
    return dispenserState;
}

public void press() {
    capacity--;
    if (capacity <= 0) {
        setState(new NullState());
    }
    dispenserState.press();
}
}

複製代碼

接着我們再進行測試:
複製代碼

public class Test {
public static void main(String[] args) {
    WaterDispenser dispenser = new WaterDispenser(new FullState());
    for (int i = 0; i < 100; ++i) {
        dispenser.press();
    }
}
}

複製代碼

這是一個非常簡單的應用場景:我們不斷的press,飲水機裏的水會越來越少,從滿桶狀態變成空桶狀態。

如果我們不使用狀態模式,也可以解決這個問題:
複製代碼

public class WaterDispenser {
private static int capacity = 20;

public void press() {
    capacity--;
    if (capacity <= 0) {
        System.out.println("There is not water poured!");
    } else {
        System.out.println("Water is pouring!");
    }
}
}

複製代碼

這樣確實是更加簡單,不需要有多餘的接口和一系列的類,但這裏的情況只有兩種,如果是三種,五種,甚至更多,幾十種,那我們到底需要多麼可怕的if…else子句啊!而且,要是條件中再嵌套條件,簡直就是難以想象!!

狀態模式的好處就是將我們從這個複雜的嵌套條件中脫離出來,但狀態模式的壞處也是非常明顯:需要管理一系列的狀態類。
狀態模式的意圖就是讓每一個狀態對修改關閉,允許對象在內部狀態改變時改變它的行爲,使對象看起來好像修改了它的類。

讓每一個狀態對修改關閉,也就是讓狀態類來改變狀態,即封裝,我們只能通過向狀態類發送消息來改變狀態,至於怎麼改變,則完全隱藏起來。

允許對象在內部狀態改變時改變它的行爲,使對象看起來好像修改了它的類。其實,對象本身擁有一個狀態類的對象集合,只要符合狀態改變的條件,就可以將表示狀態的狀態類改變成下個狀態類。從具體的代碼來看,所謂的狀態類的對象集合,其實就是我們的對象擁有一個狀態類的引用,該引用可以指向任何狀態類的對象集合的具體引用。對象本身一般都會有一個setState()方法,可以將狀態修改成另一個狀態。

至於這個對象,我們可以給它一個專門的名詞:Context,也就是上下文類。
上下文是一個難以理解的詞眼,放在具體的應用場景中更加直白點。
這裏我們的飲水機就是一個Context,它將自己的行爲委託給狀態對象執行,像是press()方法,就交給具體的狀態對象state的press()方法執行。
上下文類的明顯特點就是擁有一個引用,然後通過該引用調用相應的方法。
我們是如何區分不同的狀態?就是通過每個狀態不同的行爲來區分,所以不同的狀態都有相同的動作,但是這個動作的執行結果是不同的,而且執行結果還有可能會修改當前的狀態。爲了體現多態,擺脫對具體狀態類的依賴,使用Context可以動態的替換狀態類,就像策略模式一樣。

狀態的轉換不一定是放在Context中,有時候狀態類本身也會自動切換狀態。當狀態的轉換是固定的,像是這裏的容量爲0,就變成空桶狀態,我們可以在Context中完成轉換,但如果轉換是動態的,也就是沒有固定的判斷條件,像是一完成就自動切換,我們可以放在狀態類中。

但將狀態的切換放在狀態類中,會讓狀態類間產生依賴,而且狀態類還需要擁有Context類的引用才能切換狀態,也就是採用觀察者模式的方法來通知Context類更新狀態。所以,是否要這樣做,就看具體的編程環境以及我們的經驗了。

程序員的經驗是非常重要的,它決定我們是否可以達到更高的境界,新手是在積累經驗,等累積到一定的程度,編程的時候就基本靠經驗的驅動了,什麼是安全的代碼,什麼樣的代碼擴展性好,都會在不知不覺間體現出來,不需要特意從頭腦中挖出來。

狀態對象也是可以共享的,但前提就是狀態對象不能持有它們自己的內部狀態,否則無法保證另一個線程得到的對象是正確的狀態。所以,我們如果想要共享狀態,需要把每個狀態都指定到靜態的實例變量中。

狀態模式的意圖其實非常簡單:將與狀態有關的處理邏輯分散到代表對象狀態的各個類中,但我們的Context類必須擁有這些狀態對象集合的引用,這也就引出一個問題:實例化Context類對象會導致狀態類對象集合初始化方面的問題,也就是對象的依賴問題。

爲了儘可能減少這方面的依賴,我們的Context類通常擁有的只是一個狀態類的抽象引用,然後設置一個setter以便動態的更改狀態類對象。
如果不是使用對象,而是採用常量的方法:

private static final int FULL_STATE = 0;
private static final int NULL_STATE = 1;

也可以使用enum將它們封裝起來:

enum STATE{ NULL_STATE, FULL_STATE};

採用這種做法是因爲我們需要根據當前的狀態自動跳轉到下一個狀態,比如說,飲水機可以在空桶的狀態自動加水:

if(state == NULL_STATE){
   capacity++;
   if(capacity == 20){
        state = FULL_STATE;
   }
}

這種做法非茶館常見,因爲我們只是想要有一個狀態的判斷和切換的簡單動作,並不需要特意創建一個對象,因爲這些狀態只是單純的標識,沒有任何的職責。但程序中出現過多的靜態變量總是讓人覺得這個程序的設計不具有良好的彈性,如果可以從這些狀態中提取出抽象,可以考慮使用狀態模式來優化我們的代碼結構。

之所以說狀態模式是策略模式的孿生兄弟,是因爲它們的UML圖是一樣的,但意圖卻完全不一樣,策略模式是讓用戶指定更換的策略算法,而狀態模式是狀態在滿足一定條件下的自動更換,用戶無法指定狀態,最多隻能設置初始狀態。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章