现在我们需要另外的一个模型来负责状态变迁的过程控制, 它需要足够的通用和稳定, 整个状态机的运行模式应该是这样:
//定义初始状态为shutdown
define state = shutdown;
//定义状态变迁的实例映射关系
define operations={启动:启动操作实例, 关闭:关闭操作实例...}
//接收到了消息
while receive x->{type,cmd}
//根据类型检索消息
deviceOperation = operations.get(x.type)
//强制状态检查
if deviceOperation.avaliableStatus() contains state; then
//执行指令
deviceOperation.doOperation(x.instruction)
//更新状态
state=deviceOperation.nextStatus()
endif
done
在能够按照我们想象的方式执行之前, 我们还需要一点灵活性, 将状态机的状态变迁图映射到操作模型中来, 也就是回答一个给定迁移动作的原状态和目标状态分别是什么以及如何执行的问题, 这一点可以通过配置的方式来完成, 比如:
<stateManagement>
<operation>
<eventType>boot</eventType>
<sourceStatus>
<value>powerOff</value>
</sourceStatus>
<targetStatus>started</targetStatus>
<actionExecutor>a.b.BootExecutor</actionExecutor>
</operation>
<operation>
<eventType>shutdown</eventType>
<sourceStatus>
<value>started</value>
<value>linked</value>
<value>standby</value>
<value>linkedstandby</value>
</sourceStatus>
<targetStatus>powerOff</targetStatus>
<actionExecutor>a.b.ShutDownExecutor</actionExecutor>
</operation>
....
</stateManagement>
通过解析这样的配置文件, 我们可以很容易的将操作类型与操作实例映射起来:
public class ConfiguredDeviceOperation implements DeviceOperation {
/** 操作类型 */
private final EventTypeEnum operationType;
/** 目标状态 */
private final DeviceStatus nextStatus;
private final DeviceStatus nextStatus;
/** 指令执行器 */
private final Executor executor;
/** 支持此操作的源状态集合 */
private final Set<DeviceStatus> avaliableStatus = new HashSet<DeviceStatus>();
/**
* 构造函数, 根据给定的配置服务对象构造一个设备操作对象
* @param operationType 操作类型
* @param nextStatus 下一个状态
* @param avaliableStatus 可执行操作的目标状态
* @param executor 执行器
*/
public ConfiguredDeviceOperation(final EventTypeEnum operationType, final DeviceStatus nextStatus, final
Set<DeviceStatus> avaliableStatus, final Executor executor) {
this.operationType = operationType;
this.nextStatus = nextStatus;
this.executor = executor;
this.avaliableStatus.addAll(avaliableStatus);
}
...
现在整个状态机每一条类型不同的边对应了一条配置,我们把易变的部分从状态迁移逻辑中抽离出来了,现在控制模型的代码只需要表达一个通用的执行流程,显著的增加了结构的稳定性:
//获取接受到的命令类型
final EventTypeEnum eventType = event.eventType();
//获取接受到的命令内容
final byte[] cmd = event.getInstruction();
//从加载的配置中获取设备操作实例
final DeviceOperation deviceOperation = CONFIG.get(eventType);
//判断当前状态是否可以执行操作
if(deviceOperation.avaliableStatus().contains(currentDeviceStatus)) {
deviceOperation.doOperation(cmd);
currentDeviceStatus = deviceOperation.nextStatus();
return true;
}
return false;
现在我们再回答要新增一个状态端点要做什么的问题:
- 在状态图中新增端点和边
- 对新指令添加新的指令执行器。
- 在配置中写出新增边的描述,对于已存在的边,在源状态列表中添加新的状态端点值。
归纳起来,我们需要新增指令执行器,增加和修改配置项。通过简单的改动,较大程度的消除了代码的更改,符合开闭原则,同时,对于配置项的修改,我们甚至可以更进一步,在后台中增加新的功能来辅助完成。从代码的复杂度上来看,消除了大量的分支判断,让代码更有层次,更加简洁了,读起来不再烧脑,从维护的角度看,减少代码的修改也就减少了出错的可能,从mock的角度看,我们抽象出了边的概念,使用mockito或你所熟悉的任意mock方式来修改其行为都是非常方便的。
通用化的描述-使用DSL
前面我们使用特定的java语法来实现了一个较为灵活的状态机,引入了一段xml配置文件来简化我们的实现, 但是对于描述像状态机这样有着显著领域特征的问题, 这种方式还是太程序化以及依赖编程技巧, 如果我们想要清晰的, 通用的表达我们所要解决的问题,或者想要提高开发效率,抽象构建模型,抽取公共的代码,减少重复的劳动,亦或想要灵活应对环境或平台的改变,脱离特定语言的捆绑, 那么我们可以考虑使用DSL来解决问题:
<stateManagement initial="powerOff">
<events>
<event id="deviceShutDown" type="shutdown" />
<event id="deviceBoot" type="boot" />
<event id="deviceActive" type="active" />
<event id="deviceStandBy" type="standby" />
...
</events>
<states>
<state name="powerOff">
<transition event="deviceBoot" target="started"/>
<state/>
<state name="started">
<transition event="deviceShutDown" target="powerOff"/>
<transition event="deviceStandBy" target="standby"/>
<state/>
<state name="standby">
<transition event="deviceShutDown" target="powerOff"/>
<transition event="deviceActive" target="started"/>
<state/>
...
<states/>
</stateManagement>
这里是一段状态机的外部DSL代码, 本质上就是一段XML, 我们抽取了这类问题的惯有模式,用声明的方式提供了事件类型, 状态以及此状态下可能的转换行为。这种DSL描述的控制结构可以很容易的与各种特定编程语言进行绑定,甚至可以定制化的进行代码生成。此外,从表述能力来看, 它明显会好过java实现的版本, 一个团队中的业务专家,开发,测试人员可以很容易的去阅读和理解,降低沟通和理解的成本。
结语
状态机是我们日常工作中非常常见的一种问题场景, 在这篇文章中我们对一个简单的例子进行分析并运用常见的工程手段来得出一个灵活的实现,文章中没有去谈论任何设计模式相关的东西, 而是着眼于更加本质的需求: 灵活, 简洁, 符合开闭原则 去不断的分析和改进, 最终获得一个满意的结果, 在此之外, 我们也尝试着使用外部DSL来抽取了状态机问题的惯有模式,区别于java语言版本的是, 这种方式更加注重通用化能力和信息交流的方便程度, 提供了更加可视化的,便携的解决方式。