用設計模式去掉不必的狀態變量 —— 狀態模式

這是設計模式系列的第四篇,系列文章目錄以下:設計模式

  1. 一句話總結異曲同工的設計模式:工廠模式=?策略模式=?模版方法模式bash

  2. 使用組合的設計模式 —— 美顏相機中的裝飾者模式ide

  3. 使用組合的設計模式 —— 追女孩要用的遠程代理模式函數

  4. 用設計模式去掉不必的狀態變量 —— 狀態模式佈局

業務場景

這是在UI開發中常常會遇到的場景:界面有兩種狀態,每一種狀態下界面元素對應的操做都不一樣。好比在 offline 狀態下點擊大叉會直接退出應用,而在 login 狀態下點擊大叉會退出登陸。post

最簡單直觀的方案就是用 int 值來保存當前狀態,根據 int 值不一樣會運行不一樣分支的操做。ui

方案一:狀態變量 + if-else

public class MainActivity extends AppCompatActivity {
    //'離線狀態'
    private static final int STATE_OFFLINE = 0;
    //'登錄狀態'
    private static final int STATE_LOGIN = 1;
    //'當前狀態'
    private int currentState = STATE_OFFLINE;
    //顯示狀態的控件
    private TextView tvState;

    //省略了設置佈局文件和設置點擊監聽

    //'當按鈕點擊時執行的操做'
    public void onButtonClick() {
        if (currentState == STATE_OFFLINE) {
            logIn();
            setStateText("login");
            setState(STATE_LOGIN);
        }
    }

    //'當大叉被點擊時執行的操做'
    public void onCloseClick() {
        if (currentState == STATE_OFFLINE) {
            finish();
        } else if (currentState == STATE_LOGIN) {
            logOut();
            setStateText("offline");
            setState(STATE_OFFLINE);
        } 
    }

    public void setStateText(String state) {
        tvState.setText(state);
    }

    //'設置當前狀態'
    public void setState(int state) {
        this.currentState = state;
    }
}
複製代碼

簡單直觀,狀態變量配合 if-else 就能實現需求。this

新須要來了,新增羣組功能,當登錄成功後,再次點擊登錄按鈕就能加入羣組。在羣組時點擊大叉會退出羣組。spa

新需求增長了一種狀態,界面上的兩個操做按鈕也所以增長了兩種新的操做。設計

小場面,只須要新增 if-else 就能搞定:

public class MainActivity2 extends AppCompatActivity {
    private static final int STATE_OFFLINE = 0;
    private static final int STATE_LOGIN = 1;
    //'新增羣組狀態'
    private static final int STATE_IN_GROUP = 2;
    private int currentState = STATE_OFFLINE;
    private TextView tvState;

    public void onButtonClick() {
        if (currentState == STATE_OFFLINE) {
            logIn();
            setStateText("login");
            setState(STATE_LOGIN);
        }
        //'按鈕新增對羣組狀態的響應代碼'
        else if (currentState == STATE_LOGIN) {
            joinGroup();
            setStateText("in group");
            setState(STATE_IN_GROUP);
        }
    }

    public void onCloseClick() {
        if (currentState == STATE_OFFLINE) {
            finish();
        } else if (currentState == STATE_LOGIN) {
            logOut();
            setStateText("offline");
            setState(STATE_OFFLINE);
        } 
        //'大叉新增對羣組狀態的響應代碼'
        else if (currentState == STATE_IN_GROUP) {
            quitGroup();
            tvState.setText("login");
            setState(STATE_LOGIN);
        }
    }
複製代碼

目前看起來還不是太糟,但隨着狀態的增長,if-else 分支就會原來越多,代碼可讀性會持續降低。

更關鍵的是這不符合開閉原則,即當新增功能的時候不容許修改原有代碼。而在 demo 中新增狀態的時候,不得不修改onCloseClick()onButtonClick。demo 中的邏輯很是簡單,這兩個函數的調用者只有一個,分別是按鈕和大叉。真實項目中調用者可能分佈在各個角落,對於這種函數,你敢輕易改嗎?一不當心就可能修改出 bug 。

若是需求變動:在離線狀態增長確認,即離線時點擊按鈕彈框確認是否須要登陸,點擊大叉彈框確認是否須要退出應用。若是使用上述方案,就須要全局搜索STATE_OFFLINE,找到全部訪問它的地方,一個個的作修改(可能散佈在 n 個類中,增長了 n 個類出 bug 的可能性)。

吐槽完缺點後,看看狀態模式是怎麼解決問題的。

方案二:狀態模式

在這個場景中,變化的是狀態,增長一層抽象把變化封裝起來是設計模式的慣用手段。看下如何把狀態封裝起來:

public interface State {
    void onCloseClick();
    void onButtonClick();
}
複製代碼

新增一層抽象,這層抽象的實例表示一個具體的狀態,抽象中的方法表示該狀態能夠執行的操做。

如今有離線、登錄、進羣組這三個狀態,分別對應着三個State實例:

//'離線狀態'
public class OfflineState implements State {
    private MainActivity mainActivity;
    public OfflineState(MainActivity mainActivity) {
        this.mainActivity = mainActivity;
    }

    @Override
    public void onCloseClick() {
        mainActivity.finish();
    }

    @Override
    public void onButtonClick() {
        mainActivity.logIn();
        mainActivity.setState(mainActivity.getLoginState());
        mainActivity.setStateText("login");
    }
}

//'登錄狀態'
public class LoginState implements State {
    private MainActivity mainActivity;
    public LoginState(MainActivity activity) {
        this.mainActivity = activity;
    }

    @Override
    public void onCloseClick() {
        mainActivity.logOut();
        mainActivity.setState(mainActivity.getOfflineState());
        mainActivity.setStateText("offline");
    }

    @Override
    public void onButtonClick() {
        mainActivity.joinGroup();
        mainActivity.setState(mainActivity.getInGroupState());
        mainActivity.setStateText("in group");
    }
}

//'進羣組狀態'
public class InGroupState implements State {
    private MainActivity mainActivity;
    public InGroupState(MainActivity mainActivity) {
        this.mainActivity = mainActivity;
    }

    @Override
    public void onCloseClick() {
        mainActivity.quitGroup();
        mainActivity.setState(mainActivity.getLoginState());
        mainActivity.setStateText("login");
    }

    @Override
    public void onButtonClick() {}
}
複製代碼

MainActivity頁面持有各個狀態的實例

public class MainActivity extends AppCompatActivity {
    //'離線狀態實例'
    private State offlineState;
    //'登錄狀態實例'
    private State loginState;
    //'進羣組狀態實例'
    private State inGroupState;
    //'當前狀態'
    private State currentState;
    private TextView tvState;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        //省略了佈局和設置監聽器
        initState();
    }

    //'初始化狀態'
    private void initState() {
        offlineState = new OfflineState(this);
        loginState = new LoginState(this);
        inGroupState = new InGroupState(this);
        setStateText("offline");
        setState(offlineState);
    }

    //'將點擊按鈕操做委託給當前狀態'
    public void onButtonClick() {
        currentState.onButtonClick();
    }

    //'將點擊大叉操做委託給當前狀態'
    public void onCloseClick() {
        currentState.onCloseClick();
    }

    //'變動當前狀態'
    public void setState(State state) {
        this.currentState = state;
    }
    //'獲取指定狀態'
    public State getOfflineState() {
        return offlineState;
    }
    public State getLoginState() {
        return loginState;
    }
    public State getInGroupState() {
        return inGroupState;
    }
    public void setStateText(String state) {
        tvState.setText(state);
    }
}
複製代碼

這個方案的有趣之處在於:將「在每一個方法內處理不一樣狀態」 轉變成 「在同一個狀態類內部實現全部方法」。怎麼聽上去有種換湯不換藥的感受?

其實否則,狀態模式在新增狀態時,讓本來的每個狀態「對修改關閉」,讓MainActivity「對擴展開放」(由於新增狀態不要修改onCloseClick()onButtonClick()

又是一個「把變的東西封裝起來,用多態來應對變化」的設計模式。(它和工廠模式,模版方法模式,策略模式異曲同工,詳見設計模式第一篇

狀態模式 vs 策略模式

分析設計模式老是逃不掉相互比較,由於有幾個長的真的很像。策略模式的詳細講解和應用能夠分別移步這裏這裏

它們倆的實現方式和目的能夠說幾乎相同,都是經過接口定義行爲,經過組合持有行爲實例,經過多態動態地替換行爲。

但它們的適用場景略有區別:策略模式是在外部定義了一個行爲,並由外部發起一次性的行爲替換,而狀態模式在內部定義了多個行爲,並由內部緣由持續地發生行爲替換。

相關文章
相關標籤/搜索