java 設計模式 —— 淺析狀態模式

這天你早早的來到了公司,剛端上熱騰騰的熱茶,產品經理過來了——

“小顏,咱們項目新增了兩個功能,一個是點贊功能,一個是轉發功能,還有我們定期會做一個抽獎活動,所以還要新增一個抽獎功能,這個你把它做了吧,有沒有問題?”

“經理,沒有問題,不就是在點擊這些按鈕的時候判斷一下當前的登錄狀態麼,如果登錄了,那麼就可以進行這些功能,如果沒有登錄的話,那麼點擊這些功能我們就應該跳到登錄的界面,對不對?”

經理別有意味的笑了一下,拍了拍你的肩膀。你很疑惑——“我有說錯什麼麼?這不應該就是一個正常的邏輯麼”。想歸想,你趕緊寫起了代碼。

好不容易寫到了按鈕的點擊事件那塊 ——

@Override
public void onClick(View view) {
    switch(view.getId()) {
        // 點贊按鈕
        case R.id.btn_admire:
            if (isLogin) {
                // 已登錄
                // 實現點讚的邏輯代碼
                // ...
            } else if (!isLogin) {
                // 未登錄
                // 跳轉到登錄界面,便於用戶登錄
                startActivity(new Intent(MainActivity.this, LoginActivity.class));
            }
            break;

        // 轉發按鈕
        case R.id.btn_relay:
            if (isLogin) {
                // 已登錄
                // 實現轉發的邏輯代碼
                // ...
            } else if (!isLogin) {
                // 未登錄
                // 跳轉到登錄界面,便於用戶登錄
                startActivity(new Intent(MainActivity.this, LoginActivity.class));
            }
            break;

        // 抽獎按鈕
        case R.id.btn_raffle:
            if (isLogin) {
                // 已登錄
                // 實現抽獎的邏輯代碼
                // ...
            } else if (!isLogin) {
                // 未登錄
                // 跳轉到登錄界面,便於用戶登錄
                startActivity(new Intent(MainActivity.this, LoginActivity.class));
            }
            break;

        default:
            break;
    }

你高高興興地把代碼交給了項目經理,經理一笑,說道:“小顏啊,可能是我之前沒跟你說清楚,有些用戶在填寫註冊信息的時候,地址欄那一層是沒有填寫的,那這樣的話,我們的禮品是發不到用戶手上的,所以即使是登錄狀態,我們還需要對用戶是否填寫了地址欄信息那塊做一個判斷,如果沒有填寫地址信息,我們應該讓他跳到個人信息頁面來填寫地址欄信息。”

你拍拍胸脯道,“小意思經理,我再加一個 boolean 值不就可以了麼?”經理笑了一笑,沒有說什麼。

你感嘆道:“還好只需要改抽獎功能這一塊代碼。”沒過多久你就寫好了 ——

@Override
public void onClick(View view) {
    switch(view.getId()) {
        // 點贊按鈕
        // ...

        // 轉發按鈕
        // ...

        // 抽獎按鈕
        case R.id.btn_raffle:
            if (isLogin && isFinishedAddress) {
                // 已登錄,並且已經填寫地址欄
                // 實現抽獎的邏輯代碼
                // ...
            } else if (!isLogin) {
                // 未登錄
                // 跳轉到登錄界面,
                startActivity(new Intent(MainActivity.this, LoginActivity.class));
            } else if (isLogin && !isFinishedAddress) {
                // 已登錄,但是沒有填寫狀態欄信息
                // 跳轉到個人信息頁面,方便用戶填寫地址欄信息
                startActivity(new Intent(MainActivity.this, PersonInfoActivity.class))
            }
            break;

        default:
            break;
    }

這次你信心滿滿地找到經理,“經理,我做完了!”經理看了你修改之後的代碼說道,“小顏啊,你的代碼沒有問題,但是你不覺得到處都是 if else 的判斷邏輯,這樣的代碼很不便於閱讀麼?而且目前我們只有三個功能,假如我們後期有第四個第五個功能,假設第四個功能是需要用戶登錄並且上傳了頭像才能正常實現,如果登陸了沒有上傳頭像,那麼要跳到相應的界面,第五個功能是需要用戶驗證了郵箱,如果沒有驗證郵箱,我們會自動打開一個瀏覽器讓用戶去進行郵箱驗證,那麼你想想會有多少個 if else,而且後期需要對原有的代碼進行修改,這可是我們的大忌,回去好好看一下狀態模式吧。”“好的經理。”你迅速回去打開電腦百度到了一些信息 ——

狀態模式 —— 允許一個對象在其內部狀態改變時改變它的行爲。對象看起來似乎修改了它的類

這句話看起來十分的乾澀,你決定在實踐中來理解它。首先定義一個接口以封裝使用上下文環境的的一個特定狀態相關的行爲 —— 通俗來說,就是這個接口中有上面的點贊功能轉發功能抽獎功能,接口如下:

public interface UserState {
    // 點贊功能
    void admire(Context context);

    // 轉發功能
    void relay(Context context);

    // 抽獎功能
    void raffle(Context context);
}

接下來就是針對不同的狀態實現這個接口了,你啪啪啪起鍵盤,首先是登錄狀態的實現:

public class LoginState implements UserState {
    @Override
    public void admire(Context context) {
        // 實現點贊功能的代碼
        // ...
    }

    @Override
    public void relay(Context context) {
        // 實現轉發功能的代碼
        // ...
    }

    @Override
    public void raffle(Context context) {
        // 實現抽獎功能的代碼
        // ...
    }
}

其次是非登錄狀態的實現,代碼如下:

public class LogoutState implements UserState {
    @Override
    public void admire(Context context) {
        // 跳轉到登錄界面
        start2loginActivity(context);
    }

    @Override
    public void relay(Context context) {
        // 跳轉到登錄界面
        start2loginActivity(context);
    }

    @Override
    public void raffle(Context context) {
        // 跳轉到登錄界面
        start2loginActivity(context);
    }

    // 跳轉到登錄界面
    private void start2loginActivity(Context context) {
        gotoLoginActivity(context, LoginActivity.class);
    }
}

接下來就是用戶登錄但是沒有填寫地址欄信息的具體實現:

public class AddressState implements UserState {
    @Override
    public void admire(Context context) {
        // 跳轉到個人信息頁面
        start2personInfoActivity(context);
    }


    @Override
    public void relay(Context context) {
        // 跳轉到個人信息頁面
        start2personInfoActivity(context);
    }

    @Override
    public void raffle(Context context) {
        // 跳轉到個人信息頁面
        start2personInfoActivity(context);
    }

    // 跳轉到個人信息頁面
    private void start2personInfoActivity(Context context) {
        gotoPersonInfoActivity(context, PersonInfoActivity.class);
    }
}

接下來就是創建一個 Context 角色,它是用戶的操作對象和狀態管理對象,該角色會將相關的操作委託給 UserState 對象,代碼如下:

public class LoginContext {
    // 默認爲非登錄狀態
    UserState userState = new LogoutState();
    // 單例模式
    private static LoginContext loginContext = new LoginContext();

    private LoginContext() {
    }

    public static LoginContext getLoginContext() {
        return loginContext;
    }

    public void setUserState(UserState userState) {
        this.userState = userState;
    }

    // 點贊功能
    public void admire(Context context) {
        userState.admire(context);
    }

    // 轉發功能
    public void relay(Context context) {
        userState.relay(context);
    }

    // 抽獎功能
    public void raffle(Context context) {
        userState.relay(context);
    }
}

接下來就是在註冊的時候或者應用啓動的時候獲取用戶信息,然後設置相應的 UserState 即可,例如檢查到用戶欄填寫完整,那麼我們就 LoginContext.getLoginContext().setUserState(new LoginState());,而地址欄我們並沒有填寫完整,那麼我們就LoginContext.getLoginContext().setUserState(new AddressState());。或者我們在應用啓動的時候從服務器獲取信息,設置相應的 UserState。而 LoginContext 是單例的也很好理解,因爲在你的應用中,登錄狀態應該只有一種,而不存在既有登錄狀態,又有非登錄狀態,如果我們不使用單例模式的話,就會出現用戶既有登錄狀態,又有非登錄狀態,這樣是不符合正常邏輯的。

再回到最初的按鈕點擊事件,你就開始重構代碼了——

@Override
    public void onClick(View view) {
        switch(view.getId()) {
            // 點贊按鈕
            case R.id.btn_admire:
                // 獲取當前登錄狀態點贊功能的邏輯實現
                LoginContext.getLoginContext().admire(MainActivity.this);
                break;

            // 轉發按鈕
            case R.id.btn_relay:
                // 獲取當前登錄狀態轉發功能的邏輯實現
                LoginContext.getLoginContext().relay(MainActivity.this);
                break;

            // 抽獎按鈕
            case R.id.btn_raffle:
                // 獲取當前登錄狀態抽獎功能的邏輯實現
                LoginContext.getLoginContext().raffle(MainActivity.this);
                break;

            default:
                break;
    }

所以即使後期改變需求,針對一個上下文環境有需要新增加不同的行爲實現,也僅僅需要實現 UserState 接口就可以了。而並不是對原有代碼添加 if else 的判斷邏輯,那樣完全不符合對修改關閉,對擴展開放的開閉原則。

你將這份代碼鄭重地交給經理,經理這次高興地拍着你的肩膀說道“這份代碼我很滿意,很不錯,來,小顏,跟我說說你現在邏輯。”

“好的經理,我首先創建一個 UserState 接口,這個接口裏面的方法是需要針對不同的上下文環境有不同的實現,例如點贊功能,假如用戶登錄了,我們就正常實現,如果沒有登錄,那麼我們就跳轉到登錄的頁面。接下來我們創建一個上下文環境,這個上下文環境 Context 內部持有一個 UserState 對象,然後這個 Context 裏除了一個可以改變 UserStatesetter(UserState userstate) 方法之外,剩下的就是我們需要實現的那些功能,且 Context 自身是單例的,這樣就能確保應用全局中 UserState 的值是唯一的,它要麼是登錄的,要麼是非登錄的,要麼是登錄狀態但是並沒有填寫地址欄,或者後期應需求新增的其他狀態,因爲用戶的狀態只有一種可能性,不存在既登錄又不登錄的狀態。”

“非常好,看來你學到了精髓,你要繼續努力啊小夥子!”

“一定會的,經理!”你收拾收拾高高興興地繼續去寫代碼了。

UML 類圖:

這裏寫圖片描述

狀態模式的組成:

  • 環境類(Context): 定義客戶感興趣的接口。維護一個 ConcreteState 子類的實例,這個實例定義當前狀態。例如我們上面的 LoginContext

  • 抽象狀態類(State): 定義一個接口以封裝與 Context 的一個特定狀態相關的行爲。例如我們上面的 UserState

  • 具體狀態類(ConcreteState): 每一子類實現一個與 Context 的一個狀態相關的行爲。例如我們上面針對 UserState 接口實現的 LoginStateLogoutStateAddressState

狀態模式的適用範圍:

  • 一個對象的行爲取決於它的狀態, 並且它必須在運行時刻根據狀態改變它的行爲。

  • 代碼中包含大量與對象狀態有關的條件語句:一個操作中含有龐大的多分支的條件(if else(或switch case)語句,且這些分支依賴於該對象的狀態。這個狀態通常用一個或多個枚舉常量表示。通常 , 有多個操作包含這一相同的條件結構。 State 模式將每一個條件分支放入一個獨立的類中。這使得你可以根據對象自身的情況將對象的狀態作爲一個對象,這一對象可以不依賴於其他對象而獨立變化。

發佈了41 篇原創文章 · 獲贊 81 · 訪問量 15萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章