用設(shè)計模式去掉沒必要的狀態(tài)變量 —— 狀態(tài)模式

這是設(shè)計模式系列的第四篇,系列文章目錄如下:

  1. 一句話總結(jié)殊途同歸的設(shè)計模式:工廠模式=?策略模式=?模版方法模式

  2. 使用組合的設(shè)計模式 —— 美顏相機(jī)中的裝飾者模式

  3. 使用組合的設(shè)計模式 —— 追女孩要用的遠(yuǎn)程代理模式

  4. 用設(shè)計模式去掉沒必要的狀態(tài)變量 —— 狀態(tài)模式

業(yè)務(wù)場景

這是在UI開發(fā)中經(jīng)常會遇到的場景:界面有兩種狀態(tài),每一種狀態(tài)下界面元素對應(yīng)的操作都不同。比如在 offline 狀態(tài)下點擊大叉會直接退出應(yīng)用,而在 login 狀態(tài)下點擊大叉會退出登錄。

最簡單直觀的方案就是用 int 值來保存當(dāng)前狀態(tài),根據(jù) int 值不同會運(yùn)行不同分支的操作。

方案一:狀態(tài)變量 + if-else

public class MainActivity extends AppCompatActivity {
    //'離線狀態(tài)'
    private static final int STATE_OFFLINE = 0;
    //'登陸狀態(tài)'
    private static final int STATE_LOGIN = 1;
    //'當(dāng)前狀態(tài)'
    private int currentState = STATE_OFFLINE;
    //顯示狀態(tài)的控件
    private TextView tvState;

    //省略了設(shè)置布局文件和設(shè)置點擊監(jiān)聽

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

    //'當(dāng)大叉被點擊時執(zhí)行的操作'
    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);
    }

    //'設(shè)置當(dāng)前狀態(tài)'
    public void setState(int state) {
        this.currentState = state;
    }
}

簡單直觀,狀態(tài)變量配合 if-else 就能實現(xiàn)需求。

新需要來了,新增群組功能,當(dāng)?shù)顷懗晒?,再次點擊登陸按鈕就能加入群組。在群組時點擊大叉會退出群組。

新需求增加了一種狀態(tài),界面上的兩個操作按鈕也因此增加了兩種新的操作。

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

public class MainActivity2 extends AppCompatActivity {
    private static final int STATE_OFFLINE = 0;
    private static final int STATE_LOGIN = 1;
    //'新增群組狀態(tài)'
    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);
        }
        //'按鈕新增對群組狀態(tài)的響應(yīng)代碼'
        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);
        } 
        //'大叉新增對群組狀態(tài)的響應(yīng)代碼'
        else if (currentState == STATE_IN_GROUP) {
            quitGroup();
            tvState.setText("login");
            setState(STATE_LOGIN);
        }
    }

目前看起來還不是太糟,但隨著狀態(tài)的增加,if-else 分支就會原來越多,代碼可讀性會持續(xù)下降。

更關(guān)鍵的是這不符合開閉原則,即當(dāng)新增功能的時候不允許修改原有代碼。而在 demo 中新增狀態(tài)的時候,不得不修改onCloseClick()onButtonClick。demo 中的邏輯非常簡單,這兩個函數(shù)的調(diào)用者只有一個,分別是按鈕和大叉。真實項目中調(diào)用者可能分布在各個角落,對于這種函數(shù),你敢輕易改嗎?一不小心就可能修改出 bug 。

如果需求變更:在離線狀態(tài)增加確認(rèn),即離線時點擊按鈕彈框確認(rèn)是否需要登錄,點擊大叉彈框確認(rèn)是否需要退出應(yīng)用。如果使用上述方案,就需要全局搜索STATE_OFFLINE,找到所有訪問它的地方,一個個的做修改(可能散布在 n 個類中,增加了 n 個類出 bug 的可能性)。

吐槽完缺點后,看看狀態(tài)模式是怎么解決問題的。

方案二:狀態(tài)模式

在這個場景中,變化的是狀態(tài),增加一層抽象把變化封裝起來是設(shè)計模式的慣用手段??聪氯绾伟褷顟B(tài)封裝起來:

public interface State {
    void onCloseClick();
    void onButtonClick();
}

新增一層抽象,這層抽象的實例表示一個具體的狀態(tài),抽象中的方法表示該狀態(tài)可以執(zhí)行的操作。

現(xiàn)在有離線、登陸、進(jìn)群組這三個狀態(tài),分別對應(yīng)著三個State實例:

//'離線狀態(tài)'
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");
    }
}

//'登陸狀態(tài)'
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");
    }
}

//'進(jìn)群組狀態(tài)'
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頁面持有各個狀態(tài)的實例

public class MainActivity extends AppCompatActivity {
    //'離線狀態(tài)實例'
    private State offlineState;
    //'登陸狀態(tài)實例'
    private State loginState;
    //'進(jìn)群組狀態(tài)實例'
    private State inGroupState;
    //'當(dāng)前狀態(tài)'
    private State currentState;
    private TextView tvState;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        //省略了布局和設(shè)置監(jiān)聽器
        initState();
    }

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

    //'將點擊按鈕操作委托給當(dāng)前狀態(tài)'
    public void onButtonClick() {
        currentState.onButtonClick();
    }

    //'將點擊大叉操作委托給當(dāng)前狀態(tài)'
    public void onCloseClick() {
        currentState.onCloseClick();
    }

    //'變更當(dāng)前狀態(tài)'
    public void setState(State state) {
        this.currentState = state;
    }
    //'獲取指定狀態(tài)'
    public State getOfflineState() {
        return offlineState;
    }
    public State getLoginState() {
        return loginState;
    }
    public State getInGroupState() {
        return inGroupState;
    }
    public void setStateText(String state) {
        tvState.setText(state);
    }
}

這個方案的有趣之處在于:將“在每個方法內(nèi)處理不同狀態(tài)” 轉(zhuǎn)變成 “在同一個狀態(tài)類內(nèi)部實現(xiàn)所有方法”。怎么聽上去有種換湯不換藥的感覺?

其實不然,狀態(tài)模式在新增狀態(tài)時,讓原本的每一個狀態(tài)“對修改關(guān)閉”,讓MainActivity“對擴(kuò)展開放”(因為新增狀態(tài)不要修改onCloseClick()onButtonClick()

又是一個“把變的東西封裝起來,用多態(tài)來應(yīng)對變化”的設(shè)計模式。(它和工廠模式,模版方法模式,策略模式殊途同歸,詳見設(shè)計模式第一篇

狀態(tài)模式 vs 策略模式

分析設(shè)計模式總是逃不掉相互比較,因為有幾個長的真的很像。策略模式的詳細(xì)講解和應(yīng)用可以分別移步這里這里

它們倆的實現(xiàn)方式和目的可以說幾乎相同,都是通過接口定義行為,通過組合持有行為實例,通過多態(tài)動態(tài)地替換行為。

但它們的適用場景略有區(qū)別:策略模式是在外部定義了一個行為,并由外部發(fā)起一次性的行為替換,而狀態(tài)模式在內(nèi)部定義了多個行為,并由內(nèi)部原因持續(xù)地發(fā)生行為替換。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容