前言
之前我寫過一篇政策模式的文章,講的是如何靈活地改變對象的行為,今天要講的模式和政策模式非常像,它也是讓你設計出如何靈活改變對象行為的一個模式,與政策模式不同的是它是根據自身狀态而自行地改變行為,它就是狀态模式。
詳解
普通實作
首先我們來分析一個執行個體:現在的遊戲基本都有自動打怪做任務的功能,如果讓你實作這個功能你會怎麼做呢?
本篇講解的是狀态模式,當然首先應該分析其應有狀态和行為,下面是我畫的一個簡單的狀态圖:
橢圓代表的是所處狀态,指引線代表執行的行為。一開始角色處于初始狀态,什麼也不做,當玩家開啟自動任務功能時,角色就自動的接受任務,當接到殺怪的任務後,發現周圍沒有怪,就把“初始狀态”改為“未發現怪物”狀态并開始四處遊走尋找怪物,走啊走,走啊走,發現了目标怪物就将狀态修改為“發現怪物”,然後開始攻擊打怪,直到殺怪數量達到任務指定數量後,就停止打怪并将狀态修改為“任務達成”狀态,最後回到接任務那裡送出任務,角色狀态又重置為初始狀态(這裡隻是為了友善了解該模式,不要太糾結功能細節)。不難發現,在該執行個體中,我們包含了四個狀态和四個行為,任何一個行為是随時都有可能進行的,但是其表現結果卻會因為狀态的不同而有不一樣的結果,按照我們面向過程的程式設計方式也是非常容易實作的:
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方法名麼?
總結
狀态模式允許對象在内部狀态改變時改變它的行為,如果需要在多個對象間共享狀态,那麼隻需要定義靜态域即可。
狀态模式與政策模式具有相同的類圖,但它們本質的意圖是不同的。前者是封裝基于狀态的行為,并将行為委托到目前的狀态,使用者不需要知道有哪些狀态;而後者是将可以互換的行為封裝起來,然後使用委托,由客戶決定需要使用哪種行為,客戶需要知道所有的行為類。