这是设计模式系列的第四篇,系列文章目录如下:
业务场景
这是在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 策略模式
分析设计模式总是逃不掉相互比较,因为有几个长的真的很像。策略模式的详细讲解和应用可以分别移步和
它们俩的实现方式和目的可以说几乎相同,都是通过接口定义行为,通过组合持有行为实例,通过多态动态地替换行为。
但它们的适用场景略有区别:策略模式是在外部定义了一个行为,并由外部发起一次性的行为替换,而状态模式在内部定义了多个行为,并由内部原因持续地发生行为替换。