(本文由言念小文原创,转载请注明出处)
在实际工作中经常遇到某个对象,处于不同的状态有不同行为逻辑、且状态之间可以相互迁移的业务场景,特别是在开发通信协议栈类软件中尤为多见。《设计模式之禅》这本书中对状态模式有着非常详尽的讲解(目前为止我认为讲解得最好的书),但总觉得自己没能够理解透彻、灵活运用。直到今年完成了一个通信协议软件的开发,重新研究了“状态机”,然后回过头来理解当初学习的状态模式,豁然开朗。因此,本文先从状态机开始讲解,然后结合状态机详细阐述状态模式的两种实现方式,最后给出状态模式的优缺点及其使用场景。
一 案例描述
按照老风格,本文先描述一个场景案例,然后围绕案例来展开后文。相信每个人都用过手机的应用商城,通常在应用商城中会将可以安装的app以列表(listview)的形式呈现,一个应用占据列表的一个子项(item),如下图1所示:
图1
我们将注意力聚焦到item的按钮上:
a当检测到可安装的app,按钮显示“安装”;
b点击按钮,软件会去下载app安装包,这时按钮更新视图,显示“正在下载”(即安装进度);
c下载完成后,软件自动安装app,按钮显示“正在安装”;
d安装完成后,按钮显示“打开”,这时点击按钮将打开对应的app。
通常,一切顺利,我们安装一个app,按钮会经历“安装”“正在下载”“正在安装”“打开”四种状态。可惜的是,往往事有多磨:
当下载app安装包时,可能出现下载异常,这时按钮切换状态到“下载失败”,点击按钮,软件重新尝试下载,按钮切换状态到“正在下载”;
当安装app时,可能出现安装失败,这时按钮切换状态到“安装失败”,点击按钮,软件重新尝试安装,按钮切换状态到“正在安装”;
以上,便是我们更新一个软件时,可能遇到情况。后文,我们将实现上述功能的软件模块称为“app安装模块”,本文将以这个案例为基础,围绕实现“app安装模块”展开状态机和状态模式的讲解。
二 状态机
1.什么是状态机
通常我们工作中接触到的状态机都是有限状态机,那么什么是有限状态机呢?偷个懒直接百度大挪移:
有限状态机,(英语:Finite-state machine, FSM),又称有限状态自动机,简称状态机,是表示有限个状态以及在这些状态之间的转移和动作等行为的数学模型。
注意这个定义里面两个关键字“有限状态”“自动机”。“有限状态”指明状态机中的状态是有限且明确的,在案例中button的状态有:待安装、正在下载、下载失败、正在安装、安装失败、待打开。“自动机”说明状态机中:状态及状态下对应动作是在状态机内部自动转换和执行的,调用状态机的客户端,无需关心状态机内部的状态迁移和动作执行。
2.1 状态机的构成要素
状态机由以下四大要素构成:
现态(Qn) -- 当前状态机所处的状态。
次态(Qn+1) -- 状态机要迁移到的新状态。
事件(EVENT)(又称为条件) -- 状态机的触发信号;事件到来,能够触发状态机执行特定动作,或进行状态迁移,或二者皆执行;事件一般来自于状态机外部。
动作(ACTION) -- 事件到来后,状态机执行的动作,动作执行完后,状态机可迁移到新状态也可维持原状态,故而对于状态机中的某一状态,动作并非必须。
2.2 状态机的描述方式
状态机的描述方式有两种:状态迁移图和状态机表。
状态迁移图:
状态迁移图通过图形的方式来描述对象的全部状态逻辑,这种方式比较直观、清晰。状态迁移图由状态、状态迁移、事件和动作构成。其中,事件和动作写在状态迁移的带箭头线条上,如图2所示,图2为“app安装模块”状态迁移图,圆圈和双圆圈表示起始和结束状态。
图2 “app安装”状态迁移图
状态机表:
状态迁移表通过矩阵的方式,描述状态机的状态迁移与行为逻辑。状态机表有两种写法:
第一种,横竖表头都为状态,横表头为现态,竖表头为次态,现态和次态相交的单元格为事件触发后要执行的动作,如表1所示:
现态
次态
待安装
下载中
下载失败
安装中
安装失败
待打开
待安装
-
-
-
-
-
-
下载中
download()
-
download()
-
-
-
下载失败
-
undo()
-
-
-
-
安装中
install()
install()
-
-
install()
-
安装失败
-
-
-
undo()
-
-
待打开
-
-
-
undo()
-
open()
表1
注意1:途中undo()为表示只做状态转移,实际不执行其他动作。
注意2:由于不论什么状态,只要状态迁移了,都会有UI上变化,因此更新UI的动作updateView()不重复的提现啊状态机图和状态机表中。
第二种,横表头为现态,竖表头为事件触发后的动作,现态和动作相交的单元格,为次态。
现态
动作
待安装
下载中
下载失败
安装中
安装失败
待打开
download()
下载中
-
下载中
-
-
-
install()
安装中
安装中
-
-
安装中
-
open()
-
-
-
待打开
-
待打开
表2
两种状态机表各有特点,第一种比较适合状态较多的情况,第二种适合动作比较多的情况(根据小文的个人工程经验,比较推荐第二种)。从状态表中可以看到,状态表不能很好的描述出单个状态和动作的触发事件,因此通常状态表还是需要和状态迁移图结合使用的。
2.1 状态机的运行过程
状态机的运行实际是状态的迁移和对应动作的执行,这里我总结如下的运行分支:
EVENT-->ACTION // 事件触发,只执行动作,不转移状态
EVENT-->TRANS STATE // 事件触发,只转移状态,不执行动作
EVENT-->ACTION-->TRANS STATE // 事件触发,先做动作,后转移状态
需要说明的是:这里的ACTION通常都是触发状态转移必须要做的动作,如果不做,状态将无法成功迁移。比如,案例“app安装模块”从“安装”到“正在下载”状态的迁移,状态迁移前必须要执行download()动作,如果没有执行这个动作,状态是无法成功迁移的。
有人可能会有疑问,可以按照EVENT-->TRANS STATE-->ACTION运行吗,其实在实际的编码过程中,是可以的:案例中“app安装模块”先从“安装”迁移到“正在下载”状态,紧接着在“正在下载”状态下执行download()动作,这样在功能实现上与前一种运行顺序没有差异。不过,我个人更喜欢EVENT-->ACTION-->TRANS STATE这种顺序,因为这种顺序更加符合我们的自然逻辑。
三 状态模式
1 为什么要使用状态模式?
1.1 什么是状态模式?
在此,我不想套用GOF的定义,因为定义往往是总结和概括后高度提炼的概念,不太利于理解。当我们在项目开发过程中,分析某些业务对象或模块,发现他们的运行规律表现为状态机特征的时候,状态模式可能就要提上我们架构方案了。那么到底什么是状态模式呢?别急,看完后面的文章,相信你自己能总结出来。
1.2 为什么要使用状态模式
我们使用状态模式就是为了用软件实现具有状态机特征的业务对象,为什么要这样做呢?在状态机的定义一节,我们讲到“状态及状态下对应动作是在状态机内部自动转换和执行的,调用状态机的客户端,无需关心状态机内部的状态迁移和动作执行”。因此,状态模式是一种高度封装、高度解耦的、易于拓展的架构模式。这么好的模式,当然要啦。
2 使用状态模式完成案例
我们先来分解一下状态模式要达到的目标:a.状态及状态下对应动作是在状态机内部自动转换和执行的;b.调用状态机的客户端,无需关心状态机内部的状态迁移和动作执行。
要达到目标a,那么我们每种具体状态必须要进行封装:状态内部的动作和转换,是要封装在这个状态内部的,每个状态都必须至少要将以下两个要素封装其中:动作、状态转移方法。
要实现目标b,具体的各种状态就不能直接暴露给调用的客户端(Client),从Client到各具体状态(ConcreteState),中间必须要有一个对象,对各状态进行统一管理和无差别的暴露给Client,Client只需要与这个对象交互,就能触发软件模块自动正确运行。
2.1静态类图
通过目标分解,然后反向推理分析,便可以直接给出静态类图方案:
Client:调用“app安装模块”的客户端。
StateContext:状态上下文,即状态的环境类,对各个具体状态进行封装和管理,让各个具体状态无差别的曝露给客户端,StateContext曝露给客户端的永远是当前的状态。
State:抽象状态。
ConcreteState:具体状态。
2.2状态模式实现代码
2.2.1方式1
方式1按照我们常规的自然逻辑,在各个状态中按照EVENT-->ACTION-->TRANS STATE顺序运行。
第一步 定义抽象State
public abstract class State {
public StateContext stateContext;
public void setStateContext(StateContext context){
stateContext = context;
}
protected void updateView(String s) {
System.out.println("update button view = " + s);
};
protected abstract void doAction(Event e);
protected abstract void transState(Event e);
public abstract void eventChange(Event e);
}
对于状态,必须要有事件触发、执行动作、状态转移几种方法,结合本案例,还要有更新UI的方法,此外一个状态必须要持有状态环境对象stateContext,才能在状态迁移的时候,更新stateContext中的当前状态。
第二步 定义StateContext
public class StateContext {
// 当前状态
public State currState;
// 定义出所有状态
public static final StateToInstall stateToInstall = new StateToInstall();
public static final StateDownloading stateDownloading = new StateDownloading();
public static final StateDownloadFailed stateDownloadFailed = new StateDownloadFailed();
public static final StateInstalling stateInstalling = new StateInstalling();
public static final StateInstallFailed stateInstallFailed = new StateInstallFailed();
public static final StateToOpen stateToOpen = new StateToOpen();
public StateContext(State state) {
currState = state;
// context对象传递给当前状态对象
this.currState.setStateContext(this);
}
/**
* 获取当前状态
* @return 当前状态
*/
public State getCurrState() {
return currState;
}
/**
* 设置当前状态
* @param currState
*/
public void setCurrState(State currState) {
this.currState = currState;
// context对象传递给当前状态对象
this.currState.setStateContext(this);
}
/**
* 触发条件改变
* @param e
*/
public void eventChange(Event e) {
currState.eventChange(e);
}
}
注意:StateContext与State之间是聚合关系,故而在StateContext中定义出所有具体状态。
第三步 定义事件类型
这里用一个枚举类来定义触发“app安装模块”动作执行和状态迁移的事件信号
public enum Event {
EVENT_CLICK, // 按钮点击
EVENT_DOWNLOAD_FAILED, // 下载失败
EVENT_DOWNLOAD_SUCCESS, // 现在成功
EVENT_INSTALL_FAILED, // 安装失败
EVENT_INSTALL_SUCCESS, // 安装成功
}
第四步 定义具体状态类
/**
* “待安装”状态类
* @author 言念小文
*
*/
public class StateToInstall extends State{
@Override
protected void doAction(Event e) {
if(Event.EVENT_CLICK.equals(e) && !checkDownloaded()) {
System.out.println("current state = StateToInstall, "
+ "event change signal = click button, "
+ "do action download()");
updateView("下载中");
return;
}
if(Event.EVENT_CLICK.equals(e) && checkDownloaded()) {
System.out.println("current state = StateToInstall, "
+ "event change signal = click button, "
+ "do action install()");
updateView("安装中");
return;
}
}
@Override
protected void transState(Event e) {
if(Event.EVENT_CLICK.equals(e) && !checkDownloaded()) {
System.out.println("current state = StateToInstall, "
+ "event change signal = click button, "
+ "transfer state to StateDownloading");
// 状态转移后,设置状态环境类当前状态
stateContext.setCurrState(StateContext.stateDownloading);
return;
}
if(Event.EVENT_CLICK.equals(e) && checkDownloaded()) {
System.out.println("current state = StateToInstall, "
+ "event change signal = click button, "
+ "transfer state to StateInstalling");
// 状态转移后,设置状态环境类当前状态
stateContext.setCurrState(StateContext.stateInstalling);
return;
}
}
@Override
public void eventChange(Event e) {
if(!Event.EVENT_CLICK.equals(e)) {
return;
}
// 执行动作
doAction(e);
// 转移状态
transState(e);
}
private boolean checkDownloaded() {
return false;
}
}
在具体类中实现doAction(Event e)、transState(Event e)、eventChange(Event e)方法,具体类持有环境对象StateContext的实例。当外部事件信号通过StateContext传入某个具体类中,StateContext调用具体类中的eventChange(Event e)方法,eventChange(Event e)方法通过调用doAction()和transState()来实现动作的执行和状态的转移,这样具体类就将本状态执行的动作和状态迁移全部封装在具体状态类中,Client只需要调用StateContext实例,而无需关心具体的状态类。
/**
* “下载中”状态类
* @author 言念小文
*
*/
public class StateDownloading extends State{
@Override
protected void doAction(Event e) {
// 无论是下载成功或失败,无需执行其他动作,紧更新view
if(Event.EVENT_DOWNLOAD_FAILED.equals(e)) {
System.out.println("current state = StateDownloading, "
+ "event change signal = download failed, "
+ "do action nothing");
updateView("下载失败");
return;
}
if(Event.EVENT_DOWNLOAD_SUCCESS.equals(e)) {
System.out.println("current state = StateDownloading, "
+ "event change signal = download success, "
+ "do action install()");
updateView("安装中");
return;
}
}
@Override
protected void transState(Event e) {
if(Event.EVENT_DOWNLOAD_FAILED.equals(e)) {
System.out.println("current state = StateToInstall, "
+ "event change signal = click button, "
+ "transfer state to StateDownloadFailed");
// 状态转移后,设置状态环境类当前状态
stateContext.setCurrState(StateContext.stateDownloadFailed);
return;
}
if(Event.EVENT_DOWNLOAD_SUCCESS.equals(e)) {
System.out.println("current state = StateToInstall, "
+ "event change signal = click button, "
+ "transfer state to StateInstalling");
// 状态转移后,设置状态环境类当前状态
stateContext.setCurrState(StateContext.stateInstalling);
return;
}
}
@Override
public void eventChange(Event e) {
if(!Event.EVENT_DOWNLOAD_FAILED.equals(e)
&& !Event.EVENT_DOWNLOAD_SUCCESS.equals(e)) {
return;
}
// 执行动作
doAction(e);
// 转移状态
transState(e);
}
}
/**
* “下载失败”状态类
* @author 言念小文
*
*/
public class StateDownloadFailed extends State{
@Override
protected void doAction(Event e) {
if(Event.EVENT_CLICK.equals(e)) {
System.out.println("current state = StateDownloadFailed, "
+ "event change signal = click button, "
+ "do action download()");
updateView("下载中");
return;
}
}
@Override
protected void transState(Event e) {
if(Event.EVENT_CLICK.equals(e)) {
System.out.println("current state = StateDownloadFailed, "
+ "event change signal = click button, "
+ "transfer state to StateDownloading");
// 状态转移后,设置状态环境类当前状态
stateContext.setCurrState(StateContext.stateDownloading);
return;
}
}
@Override
public void eventChange(Event e) {
if(!Event.EVENT_CLICK.equals(e)) {
return;
}
// 执行动作
doAction(e);
// 转移状态
transState(e);
}
}
/**
* “安装中”状态类
* @author 言念小文
*
*/
public class StateInstalling extends State{
@Override
protected void doAction(Event e) {
if(Event.EVENT_INSTALL_FAILED.equals(e)) {
System.out.println("current state = StateInstalling, "
+ "event change signal = install failed, "
+ "do action nothing");
updateView("安装失败");
return;
}
if(Event.EVENT_INSTALL_SUCCESS.equals(e)) {
System.out.println("current state = StateInstalling, "
+ "event change signal = install success, "
+ "do action nothing");
updateView("打开");
return;
}
}
@Override
protected void transState(Event e) {
if(Event.EVENT_INSTALL_FAILED.equals(e)) {
System.out.println("current state = StateInstalling, "
+ "event change signal = install failed, "
+ "transfer state to StateInstallFailed");
// 状态转移后,设置状态环境类当前状态
stateContext.setCurrState(StateContext.stateInstallFailed);
return;
}
if(Event.EVENT_INSTALL_SUCCESS.equals(e)) {
System.out.println("current state = StateInstalling, "
+ "event change signal = install success, "
+ "transfer state to StateToOpen");
// 状态转移后,设置状态环境类当前状态
stateContext.setCurrState(StateContext.stateToOpen);
return;
}
}
@Override
public void eventChange(Event e) {
if(!Event.EVENT_INSTALL_FAILED.equals(e) &&
!Event.EVENT_INSTALL_SUCCESS.equals(e)) {
return;
}
// 执行动作
doAction(e);
// 转移状态
transState(e);
}
}
/**
* “安装失败”状态类
* @author 言念小文
*
*/
public class StateInstallFailed extends State{
@Override
protected void doAction(Event e) {
if(Event.EVENT_CLICK.equals(e)) {
System.out.println("current state = StateInstallFailed, "
+ "event change signal = click button, "
+ "do action install()");
updateView("安装中");
return;
}
}
@Override
protected void transState(Event e) {
if(Event.EVENT_CLICK.equals(e)) {
System.out.println("current state = StateInstallFailed, "
+ "event change signal = click button, "
+ "transfer state to StateInstalling");
// 状态转移后,设置状态环境类当前状态
stateContext.setCurrState(StateContext.stateInstalling);
return;
}
}
@Override
public void eventChange(Event e) {
if(!Event.EVENT_CLICK.equals(e)) {
return;
}
// 执行动作
doAction(e);
// 转移状态
transState(e);
}
}
/**
* “待打开”状态类
* @author 言念小文
*
*/
public class StateToOpen extends State{
@Override
protected void doAction(Event e) {
if(Event.EVENT_CLICK.equals(e)) {
System.out.println("current state = StateToOpen, "
+ "event change signal = click button, "
+ "do action open()");
// 点击打开,button view没有变化
updateView("打开");
return;
}
}
@Override
protected void transState(Event e) {
if(Event.EVENT_CLICK.equals(e)) {
// 状态不发生转移
stateContext.setCurrState(StateContext.stateToOpen);
return;
}
}
@Override
public void eventChange(Event e) {
if(!Event.EVENT_CLICK.equals(e)) {
return;
}
// 执行动作
doAction(e);
// 转移状态
transState(e);
}
}
第五步 定义Client,并运行程序
public class Client {
public static void main(String[] args) {
// 创建状态环境类对象,并初始化状态
StateContext context = new StateContext(StateContext.stateToInstall);
// 下载
context.eventChange(Event.EVENT_CLICK);
System.out.println("-----------------------------------------------\r");
// 下载失败
context.eventChange(Event.EVENT_DOWNLOAD_FAILED);
System.out.println("-----------------------------------------------\r");
// 重新下载
context.eventChange(Event.EVENT_CLICK);
System.out.println("-----------------------------------------------\r");
// 下载成功
context.eventChange(Event.EVENT_DOWNLOAD_SUCCESS);
System.out.println("-----------------------------------------------\r");
// 安装失败
context.eventChange(Event.EVENT_INSTALL_FAILED);
System.out.println("-----------------------------------------------\r");
// 重新安装
context.eventChange(Event.EVENT_CLICK);
System.out.println("-----------------------------------------------\r");
// 安装成功
context.eventChange(Event.EVENT_INSTALL_SUCCESS);
System.out.println("-----------------------------------------------\r");
}
}
Client类中,只需要持有StateContext,然后输入不同的事件件号,就可以出发“app安装模块”的动作执行和状态迁移。
执行结果如下:
current state = StateToInstall, event change signal = click button, do action download()
update button view = 下载中
current state = StateToInstall, event change signal = click button, transfer state to StateDownloading
-----------------------------------------------
current state = StateDownloading, event change signal = download failed, do action nothing
update button view = 下载失败
current state = StateToInstall, event change signal = click button, transfer state to StateDownloadFailed
-----------------------------------------------
current state = StateDownloadFailed, event change signal = click button, do action download()
update button view = 下载中
current state = StateDownloadFailed, event change signal = click button, transfer state to StateDownloading
-----------------------------------------------
current state = StateDownloading, event change signal = download success, do action install()
update button view = 安装中
current state = StateToInstall, event change signal = click button, transfer state to StateInstalling
-----------------------------------------------
current state = StateInstalling, event change signal = install failed, do action nothing
update button view = 安装失败
current state = StateInstalling, event change signal = install failed, transfer state to StateInstallFailed
-----------------------------------------------
current state = StateInstallFailed, event change signal = click button, do action install()
update button view = 安装中
current state = StateInstallFailed, event change signal = click button, transfer state to StateInstalling
-----------------------------------------------
current state = StateInstalling, event change signal = install success, do action nothing
update button view = 打开
current state = StateInstalling, event change signal = install success, transfer state to StateToOpen
-----------------------------------------------
2.2.2方式2
前文我们说了,在实际编码的过程中,EVENT-->TRANS STATE-->ACTION执行顺序也是可以的的,只不过需要将Action定义在次态中,然后次态中的Action要委托到现态中执行。具体的编码方式请参照《设计模式之禅道》关于状态模式的章节。
四 状态模式优缺点及使用场景
1 状态模式优缺点
优点:从前文我们已经可以看出,Client只需要持有StateContext实例,仅仅通过事件信号就可以驱动“app安装模块”的运行,无需关注软件模块内部实现,故具有很好的封装性,能很好解耦。如果要添加一种新状态,只需要添加一个新的状态子类,无需影响其他类,故而扩展性良好。
缺点:随着状态增加,状态子类会增多,导致类膨胀。
2 应用场景
个人认为,对于某个模块或者对象,其行为出现状态机特征的均可以使用该模式,以达到解耦、高扩展性、避免过多条件分支语句的目的。