前言
之前我寫過一篇策略模式的文章,講的是如何靈活地改變對象的行爲,今天要講的模式和策略模式非常像,它也是讓你設計出如何靈活改變對象行爲的一個模式,與策略模式不同的是它是根據自身狀態而自行地改變行爲,它就是狀態模式。
詳解
普通實現
首先我們來分析一個實例:現在的遊戲基本都有自動打怪做任務的功能,如果讓你實現這個功能你會怎麼做呢?
本篇講解的是狀態模式,當然首先應該分析其應有狀態和行爲,下面是我畫的一個簡單的狀態圖:
橢圓代表的是所處狀態,指引線代表執行的行爲。一開始角色處於初始狀態,什麼也不做,當玩家開啓自動任務功能時,角色就自動的接受任務,當接到殺怪的任務後,發現周圍沒有怪,就把“初始狀態”改爲“未發現怪物”狀態並開始四處遊走尋找怪物,走啊走,走啊走,發現了目標怪物就將狀態修改爲“發現怪物”,然後開始攻擊打怪,直到殺怪數量達到任務指定數量後,就停止打怪並將狀態修改爲“任務達成”狀態,最後回到接任務那裏提交任務,角色狀態又重置爲初始狀態(這裏只是爲了方便理解該模式,不要太糾結功能細節)。不難發現,在該實例中,我們包含了四個狀態和四個行爲,任何一個行爲是隨時都有可能進行的,但是其表現結果卻會因爲狀態的不同而有不一樣的結果,按照我們面向過程的編程方式也是非常容易實現的:
public class Character {
// 停止
private final static int STOP = 0;
// 附近有怪
private final static int HASMONSTER = 1;
// 附近沒有怪
private final static int NOMONSTER = 2;
// 任務條件達成
private final static int MISSIONCLEAR = 4;
// 當前狀態
private int state = STOP;
// 還需殺怪數量
private int count = 0;
public void accept(int count) {
if (state == STOP) {
this.count = count;
state = NOMONSTER;
// move to find the monster
move();
} else if (state == HASMONSTER) {
System.out.println("Sorry!You are doing the task,so you can't accept the new task!!");
} else if (state == NOMONSTER) {
System.out.println("Sorry!You are doing the task,so you can't accept the new task!!");
} else if (state == MISSIONCLEAR) {
System.out.println("Sorry!You must submit the current task!");
}
}
private void move() {
if (state == STOP) {
System.out.println("Moving....");
state = HASMONSTER;
attack();
} else if (state == HASMONSTER) {
System.out.println("Moving to find new monster");
attack();
} else if (state == NOMONSTER) {
System.out.println("Moving to find monster");
state = HASMONSTER;
attack();
} else if (state == MISSIONCLEAR) {
System.out.println("Moving to submit");
submit();
}
}
private void attack() {
}
private void submit() {
}
}
最後兩個方法我沒有給出具體實現,相信難不倒你,當全部實現后角色就能自動接任務打怪了:
Accept the task.Need to kill monster:10
Moving to find monster
need to kill:9
need to kill:8
need to kill:7
need to kill:6
need to kill:5
need to kill:4
need to kill:3
need to kill:2
need to kill:1
need to kill:0
Moving to submit
Congratulations on completing the task!
不過,功能雖然實現了,但是這樣寫代碼冗長不說,還非常難於理解維護,想象一下這裏只假設了4種狀態,當如果有非常多的狀態,那就是滿篇的if else了,而且如果未來需要增加新的狀態,那麼當前的實現無疑是違反了open-close原則的,我們沒有封裝變化的那部分。那應該如何做呢?這就需要我們的狀態模式了。
使用狀態模式重構代碼
往下看之前,不妨先仔細思考一下,既然該功能中狀態是會隨時改變的,而行爲又會受到狀態的影響,那何不將狀態抽離出來成爲一個體系呢?比如定義一個狀態接口(爲什麼這裏需要定義所有的行爲方法呢?):
public interface State {
void accept(int count);
void move();
void attack();
void submit();
}
那麼角色類中就可以如下定義了:
public class Character {
// 當前狀態
private State current = new StopState(this);
// 所需殺怪數量
private int count = 0;
public void accept(int count) {
// 注意這裏不能直接將值賦給成員變量
current.accept(count);
}
public void move() {
current.move();
}
public void attack() {
current.attack();
}
public void submit() {
current.submit();
}
public void killOne() {
this.count--;
}
public void setCurrent(State current) {
this.current = current;
}
public void setCount(int count) {
this.count = count;
}
public State getCurrent() {
return current;
}
public int getCount() {
return count;
}
}
相比較之前,新的類只保留了當前狀態,並增加了getter和setter方法,而角色的行爲則全都委託給了具體的狀態類來實現,那具體的狀態類應該如何實現呢?
// 初始狀態
public class StopState implements State {
private Character c;
public StopState(Character c) {
this.c = c;
}
@Override
public void accept(int count) {
c.setCount(count);
c.setCurrent(new NoMonsterState(c));
c.move();
}
@Override
public void move() {
System.out.println("Moving....");
c.setCurrent(new HasMonsterState(c));
c.attack();
}
@Override
public void attack() {
System.out.println("Sorry!You must accept the task!");
}
@Override
public void submit() {
System.out.println("You don't have task to submit!");
}
}
// 附近沒有怪物
public class NoMonsterState implements State {
private Character c;
public NoMonsterState(Character c) {
this.c = c;
}
@Override
public void accept(int count) {
System.out.println("Sorry!You are doing the task,so you can't accept the new task!!");
}
@Override
public void move() {
System.out.println("Moving to find monster!");
c.setCurrent(new HasMonsterState(c));
c.attack();
}
@Override
public void attack() {
c.move();
}
@Override
public void submit() {
System.out.println("Please complete the task!");
}
}
這裏我也只給出了兩個實現類,其它的相信你能很容實現它們。通過狀態模式重構後,代碼清晰了很多,沒有滿屏的if else,角色也能夠根據當前所處的狀態表現出相應的行爲,同時如果需要增加新的狀態時,只需要實現State接口就行了,看起來相當完美。但是,沒有什麼模式是完美的,使用狀態模式的缺點我們很容易發現,原來一個類就能解決的,現在裂變爲了四個類,系統結構複雜了很多,但這樣的犧牲是非常有必要和值得的。
思考
剛剛我們已經實現了狀態模式,但是還有個細節問題不知你注意到了沒有?比如:
public void move() {
System.out.println("Moving....");
c.setCurrent(new HasMonsterState(c));
c.attack();
}
在我的實現中,都是由狀態來控制下一個狀態是什麼,這樣狀態之間就形成了強依賴,當然你可以將狀態轉換放到context(Character)類中,不過這種更適合狀態轉換是固定的,而在我們這個例子中,狀態的變更是動態的。還需要注意的是我這裏調用 c.setCurrent(new HasMonsterState©)時,狀態是硬編碼傳入的,這樣當系統進化時可能就需要更改此處的代碼,如何解決這種情況呢?在《Head First設計模式》書中有提到,在Context類中定義所有的狀態並提供getter方法,這裏則調用getter獲取後再傳入,但區別只在於是context類還是狀態類對修改封閉:
c.setCurrent(c.getHasMonsterState());
對此我有點疑問,即使使用getter獲取,那未來系統進化導致狀態的改變後難道不需要修改getter方法名麼?
總結
狀態模式允許對象在內部狀態改變時改變它的行爲,如果需要在多個對象間共享狀態,那麼只需要定義靜態域即可。
狀態模式與策略模式具有相同的類圖,但它們本質的意圖是不同的。前者是封裝基於狀態的行爲,並將行爲委託到當前的狀態,用戶不需要知道有哪些狀態;而後者是將可以互換的行爲封裝起來,然後使用委託,由客戶決定需要使用哪種行爲,客戶需要知道所有的行爲類。