1.狀態模式介紹
狀態模式中行爲是由狀態來決定的,不同的狀態下有不同的行爲。狀態模式和策略模式的結構幾乎完全一樣,但它們的目的、本質卻完全不一樣。狀態模式的行爲是平行的、不可替換的,策略模式的行爲是彼此獨立、可相互替換的。用一句話來表述,狀態模式把對象的行爲包裝在不同的狀態對象裏,每一個狀態對象都有一個共同的抽象狀態基類。狀態模式的意圖是讓一個對象在其內部狀態改變的時候,其行爲也隨之改變。
2.狀態模式的定義
當一個對象的內在狀態改變時允許改變其行爲,這個對象看起來像是改變了其類。
3.狀態模式的使用場景
- 一個對象的行爲取決於它的狀態,並且它必須在運行時根據狀態改變它的行爲。
- 代碼中包含大量與對象狀態有關的條件語句,例如,一個操作中含有龐大的多分支語句(if-else或switch-case),且這些分支依賴於該對象的狀態。
狀態模式將每一個條件分支放入一個獨立的類中,這使得你可以根據對象自身情況將對象的狀態作爲一個對象,這一對象可以不依賴於其它對象而獨立變化,這樣通過多態來去除多的、重複的if-else等分支語句。
4.狀態模式的UML圖
UML類圖如下:
角色介紹:
- Context:環境類,定義客戶感興趣的接口,維護一個State子類的實例,這個實例定義了對象的當前狀態。
- State:抽象狀態類或者狀態接口,定義一個或者一組接口,表示該狀態下的行爲。
- ConcreteStateA、ConcreteStateB:具體狀態類,每一個具體的狀態類實現抽象State中定義的接口,從而大道不同狀態下的不同行爲。
5.狀態模式的簡單示例
下面我們就以電視遙控器爲例來演示一下狀態模式的實現。我們首先將電視的狀態簡單分爲開機狀態和關機狀態,在開機狀態下可以通過遙控器進行頻道切換、調整音量等操作,但是,此時重複按開機鍵是無效的;而在關機狀態下,頻道切換、調整音量、關機都是無效的操作,只有按開機按鈕時會生效。也就是說電視的內部狀態決定了遙控器的行爲,我們看看第一版實現:
/*
* 電視遙控器,含有開機、關機、下一頻道、上一頻道、調高音量、調低音量這幾個功能
*/
public class TvController {
//開機狀態
private final static int POWER_ON=1;
//關機狀態
private final static int POWER_OFF=2;
private int mState=POWER_OFF;
public void powerOn(){
mState=POWER_ON;
if (mState==POWER_OFF) {
System.out.println("開機了");
}
}
public void powerOff(){
mState=POWER_OFF;
if (mState==POWER_ON) {
System.out.println("關機啦");
}
}
public void nextChannel(){
if (mState==POWER_ON) {
System.out.println("下一個頻道");
}else{
System.out.println("兩個紅燈提示沒有開機");
}
}
public void prevChannel(){
if (mState==POWER_ON) {
System.out.println("上一個頻道");
}else{
System.out.println("兩個紅燈提示沒有開機");
}
}
public void turnUp(){
if (mState==POWER_ON) {
System.out.println("調高音量");
}else{
System.out.println("兩個紅燈提示沒有開機");
}
}
public void turnDown(){
if (mState==POWER_ON) {
if (mState==POWER_ON) {
System.out.println("調低音量");
}else{
System.out.println("兩個紅燈提示沒有開機");
}
}
}
}
可以看到,在TvController類中,通過mState字段存儲了電視的狀態,並且在各個操作中根據狀態來判斷是否應該執行。這就導致你了在每個功能中都需要使用if-else,代碼重複、相對較爲混亂,這是在只有兩個狀態和簡單幾個功能函數的情況下,那麼當狀態變成5個、功能函數變爲10個呢?每個函數中都要用if-else進行判斷,而這些代碼都充斥在一個類中,這些重複的代碼無法被提取出來,這使得這個類變得越來越難以維護。
狀態模式就是爲了解決這類問題而出現的,我們將這些狀態用對象來代替,將這些行爲封裝到對象中,使得在不同的狀態下有不同的實現,這樣就將這些if-else從TvController類中去掉,整個結構也變得清晰起來。
//電視狀態接口,定義了電視操作的函數
public interface TvState {
public void nextChannel();
public void prevChannel();
public void turnUp();
public void turnDown();
}
//關機狀態,此時只有開機功能是有效的
public class PowerOffState implements TvState{
@Override
public void nextChannel() {
}
@Override
public void prevChannel() {
}
@Override
public void turnUp() {
}
@Override
public void turnDown() {
}
}
//開機狀態,此時再觸發開機功能不做任何操作
public class PowerOnState implements TvState {
@Override
public void nextChannel() {
System.out.println("下一頻道");
}
@Override
public void prevChannel() {
System.out.println("上一頻道");
}
@Override
public void turnUp() {
System.out.println("調高音量");
}
@Override
public void turnDown() {
System.out.println("調低音量");
}
}
//電源操作接口
public interface PowerController {
public void powerOn();
public void powerOff();
}
//電視遙控器,類似於經典狀態模式中的Context
public class TvController implements PowerController {
TvState mTvState;
public void setmTvState(TvState mTvState) {
this.mTvState = mTvState;
}
@Override
public void powerOn() {
setmTvState(new PowerOnState());
System.out.println("開機啦");
}
@Override
public void powerOff() {
setmTvState(new PowerOffState());
System.out.println("關機啦");
}
public void nextChannel() {
mTvState.nextChannel();
}
public void prevChannel() {
mTvState.prevChannel();
}
public void turnUp() {
mTvState.turnUp();
}
public void turnDown() {
mTvState.turnDown();
}
}
下面是客戶端調用的代碼;
public class Client {
public static void main(String[] args) {
TvController tvController=new TvController();
//設置開機狀態
tvController.powerOn();
//下一個頻道
tvController.nextChannel();
//調高音量
tvController.turnUp();
//設置關機
tvController.powerOff();
//調高音量,此時不會生效
tvController.turnUp();
}
}
輸出結果如下:
開機啦
下一頻道
調高音量
關機啦
上述實現中,我們抽象了一個TvState接口,該接口中有操作電視的所有函數,該接口有兩個實現類,即開機狀態(PowerOnState)和關機狀態(PowerOffState)。開機狀態下只有開機功能是無效的,也就是說在已經開機的時候用戶在按下開機鍵不會產生任何反應;而在關機狀態下,只有開機功能是可用的,其它功能都不會生效。同一個操作,如調高音量的turnUp函數,在關機狀態下無效,在開機狀態下就會將電視的音量調高,也就是說電視的內部狀態影響了電視遙控器的行爲。狀態模式將這些行爲封裝到狀態類中,在進行操作時將這些功能轉發給狀態對象,不同的狀下有不同的實現,這樣就通過多態的形式去除了重複、雜亂的if-else語句,這也是狀態模式的精髓所在。
6.狀態模式實戰
在開發過程中,我們用到狀態模式最常見的地方應該是用戶登錄系統。在用戶已登錄和未登錄的情況下,對於同一事件的處理行爲是不一樣的,例如,在新浪微博中,用戶在未登錄的情況下點擊轉發按鈕,此時會先讓用戶登錄,然後再執行操作;如果是已登錄的情況下,那麼用戶輸入轉發的內容就可以直接進行轉發。
總結
狀態模式的關鍵點在於不同的狀態下對於同一行爲有不同的響應,這其實就是一個將if-else用多態來實現的一個具體示例。在if-else或者switch-case形式下根據不同的狀態進行判斷,如果是狀態A那麼執行方法A、狀態B執行方法B,但這種實現使得邏輯耦合在一起,易於出錯,通過狀態模式能夠很好地消除這類“醜陋”的邏輯處理,當然並不是任何出現if-else的地方都應該通過狀態模式重構,模式的運用一定要考慮所處的情景以及你要解決的問題,只有符合特定的場景才建議使用對應的模式。
狀態模式的優點:
State模式將所有與一個特定的狀態相關的行爲都放入一個狀態對象中,它提供了一個更好的方法來組織與特定狀態相關的代碼,將繁瑣的狀態轉換成結構清晰的狀態類族,在避免代碼膨脹的同時也保證了可擴展性與可維護性。
狀態模式的缺點:
狀態模式的使用必然會增加系統類和對象個數。