隨遇而安——狀態模式

HunterArley發表於2019-01-25

《Android原始碼設計模式解析與實戰》讀書筆記(八) 《Android原始碼設計模式解析與實戰》PDF資料下載

一、狀態模式的簡介

狀態模式中的行為是由狀態來決定的,不同的狀態下有不同的行為。狀態模式和策略模式的結構幾乎完全一樣,但它們的目的、本質卻完全不一樣。

狀態模式的行為是平行的、不可替換的,策略模式的行為是彼此獨立、可相互替換的。

狀態模式把物件的行為包裝在不同的狀態物件裡,每一個狀態物件都有一個共同的抽象狀態基類。

狀態模式的意圖是讓一個物件在其內部狀態改變的時候,其行為也隨之改變。

1.1、定義

當一個物件的內在狀態改變時允許改變其行為,這個物件看起來像是改變了其類。

1.2、使用場景

  1. 一個物件的行為取決於它的狀態,並且它必須在執行時根據狀態改變它的行為。
  2. 程式碼中包含大量與物件狀態有關的條件語句,例如,一個操作中含有龐大的多分支語句,且這些分支依賴於該物件的狀態。

狀態模式將每一個條件分支放入一個獨立的類中,這使得我們可以根據物件自身的情況將物件的狀態作為一個物件,這一物件可以不依賴於其他物件而獨立變化,通過多型來去除過多的、重複的if-else等分支語句。

二、狀態模式的簡單示例

下面以電視遙控器為例來演示一下狀態模式的實現。

第一版程式碼:

/**
 * 電視遙控器,含有開機、關機、下一頻道、上一頻道、調高音量、調低音量這幾個功能
 */
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) {
            System.out.println("調低音量");
        } else {
            System.out.println("兩個紅燈提示沒有開機");
        }
    }
}
複製程式碼

在TvController類中,通過mState欄位儲存了電視的狀態,並且在各個操作中根據狀態來判斷是否執行。因此導致了在每個功能中都需要使用if-else,程式碼重複、相對較為混亂。

狀態模式即使為解決這類的問題而出現的,如下程式碼:

//電視狀態介面,定義了電視操作的函式
public interface TvState {
    public void nextChannel();
    public void prevChannel();
    public void turnUp();
    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 class PowerOffState implements TvState {
    @Override
    public void nextChannel() {

    }

    @Override
    public void prevChannel() {

    }

    @Override
    public void turnUp() {

    }

    @Override
    public void turnDown() {

    }
}
複製程式碼
/**
 * 電源操作介面
 */
public interface PowerController {
    public void powerOn();

    public void powerOff();
}
複製程式碼
/**
 * 電視遙控器,類似於經典狀態模式中的Context
 */
public class TvController implements PowerController {
    TvState mTvState;

    public void setTvState(TvState mTvState) {
        this.mTvState = mTvState;
    }

    @Override
    public void powerOn() {
        setTvState(new PowerOnState());
        System.out.println("開機了!");
    }

    @Override
    public void powerOff() {
        setTvState(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();
    }
}
複製程式碼

呼叫程式碼:

TvController tvController = new TvController();
//設定開機狀態
tvController.powerOn();
//下一頻道
tvController.nextChannel();
//調高音量
tvController.turnUp();
//設定關機狀態
tvController.powerOff();
//調高音量,此時不會生效
tvController.turnUp();
複製程式碼

輸出結果:

狀態模式.png

三、狀態模式實戰

在開發過程中,用到狀態模式最常見的地方應該是使用者登入系統。在使用者已登入和未登入的情況下,對於同一事件的處理行為是不一樣的。

使用者的預設狀態為未登入狀態,此時使用者在MainActivity介面點選轉發時會先跳轉到登入介面,然後在登入介面登陸成功後再回到MainActivity頁面,此時使用者字啊進行其他操作就可以實現真正的功能。

MainActivity的程式碼如下:

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        //轉發按鈕
        findViewById(R.id.forward_btn).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                //呼叫LoginContext的轉發函式
                LoginContext.getLoginContext().forward(MainActivity.this);
            }
        });

        //登出功能
        findViewById(R.id.logout_btn).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                //設定為登出狀態
                LoginContext.getLoginContext().setState(new LogoutState());
            }
        });
    }
}
複製程式碼

LoginActivity的程式碼如下:

public class LoginActivity extends AppCompatActivity {
    EditText userNameEditText,passwordEditText;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_login);
        userNameEditText=(EditText)findViewById(R.id.username_edittext);
        passwordEditText = (EditText) findViewById(R.id.password_edittext);
        //登入按鈕
        findViewById(R.id.login_btn).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                login();
                finish();
            }
        });
    }

    private void login() {
        String userName = userNameEditText.getText().toString().trim();
        String pwd = passwordEditText.getText().toString().trim();
        //執行網路請求,進行登入.....

        //登入成功後修改為已登入狀態
        LoginContext.getLoginContext().setState(new LoginedState());
        Toast.makeText(getApplicationContext(), "登陸成功", Toast.LENGTH_SHORT).show();
    }
}
複製程式碼

LoginContext的程式碼:

/**
 * 使用者介面和狀態管理類
 */
public class LoginContext {
    //使用者狀態,預設為未登入狀態
    UserState mState = new LogoutState();
    //單例
    private static LoginContext sLoginContext = new LoginContext();

    private LoginContext() {
    }

    public static LoginContext getLoginContext() {
        return sLoginContext;
    }

    public void setState(UserState state) {
        mState = state;
    }

    //轉發
    public void forward(Context context) {
        mState.forward(context);
    }

    //評論
    public void comment(Context context) {
        mState.comment(context);
    }
}
複製程式碼

使用者狀態介面程式碼:

/**
 * 使用者狀態
 */
public interface UserState {
    /**
     * 轉發
     */
    public void forward(Context context);

    /**
     * 評論
     */
    public void comment(Context context);
}
複製程式碼

已登入狀態程式碼:

/**
 * 已登入狀態
 */
public class LoginedState implements UserState {

    @Override
    public void forward(Context context) {
        Toast.makeText(context, "轉發功能", Toast.LENGTH_SHORT).show();
    }

    @Override
    public void comment(Context context) {
        Toast.makeText(context, "評論功能", Toast.LENGTH_SHORT).show();
    }
}
複製程式碼

未登入狀態程式碼:

/**
 * 登出狀態,即未登入狀態
 */
public class LogoutState implements UserState {

    @Override
    public void forward(Context context) {
        gotoLoginActivity(context);
    }

    @Override
    public void comment(Context context) {
        gotoLoginActivity(context);
    }

    private void gotoLoginActivity(Context context) {
        Intent intent = new Intent(context, LoginActivity.class);
        context.startActivity(intent);
    }

}
複製程式碼

四、總結

狀態模式的關鍵點在於不同的狀態下對於同一行為有不同的響應,這其實就是一個將if-else用多型來實現的一個具體示例。

4.1、優點

狀態模式將所有與一個特定的狀態相關的行為都放入一個狀態物件中,它提供了一個更好的方法來組織與特定狀態的相關程式碼,將繁瑣的狀態判斷轉換為結構清晰的狀態類族,在避免程式碼膨脹的同時也保證了可擴充套件性與可維護性。

4.2、缺點

狀態模式的使用必然會增加系統類和物件的個數。

學海無涯苦作舟

我的微信公眾號

相關文章