這是設計模式系列的第四篇,系列文章目錄如下:
業務場景
這是在UI開發中經常會遇到的場景:介面有兩種狀態,每一種狀態下介面元素對應的操作都不同。比如在 offline 狀態下點選大叉會直接退出應用,而在 login 狀態下點選大叉會退出登入。
最簡單直觀的方案就是用 int 值來儲存當前狀態,根據 int 值不同會執行不同分支的操作。
方案一:狀態變數 + 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 就能實現需求。
新需要來了,新增群組功能,當登陸成功後,再次點選登陸按鈕就能加入群組。在群組時點選大叉會退出群組。
新需求增加了一種狀態,介面上的兩個操作按鈕也因此增加了兩種新的操作。
小場面,只需要新增 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 策略模式
分析設計模式總是逃不掉相互比較,因為有幾個長的真的很像。策略模式的詳細講解和應用可以分別移步這裡和這裡
它們倆的實現方式和目的可以說幾乎相同,都是通過介面定義行為,通過組合持有行為例項,通過多型動態地替換行為。
但它們的適用場景略有區別:策略模式是在外部定義了一個行為,並由外部發起一次性的行為替換,而狀態模式在內部定義了多個行為,並由內部原因持續地發生行為替換。