1. 业务背景
1. 需求
业务方希望能够对运单的揽收、干支运输、派送等各个环节的操作规范进行监控,同时将温湿监控、设备监控、配置监控等孤立的报警项映射到运单环节之上。
运单环节由运单状态映射而得,譬如干支运输环节的典型状态区间是封车–>解封车。
1.2 难点
运单环节映射最大的难点在于,无论是在系统上还是运营上,运单的各个关键状态都不是严格齐全的、有序的,存在大量的漏操作、补操作,很难甚至无法进行完全正确的环节映射——一个环节可能永远不会有对应的终止状态产生。此时,系统上要尽快识别这种异常,做到今早结束。
此外,几百种运单状态前期难以梳理清楚,业务也在不断的变化,这些都使得环节的映射关系随时可能变更。
以上种种要求系统在设计上要能够适应复杂、多变的条件判断,传统编码方式会存在大量的if-else判断,场景考虑不全、后期维护困难。
方案设计毫无眉目之时,翻看设计模式之美这本书,想从中找找灵感,万万没想到居然淘到了宝——状态机。
2. 状态机简介
详见:有限状态机
2.1 定义
有限状态机(Finite State Machine,FSM)简称状态机。状态机有三个组成部分:状态(State)、事件(Event)、动作(Action),事件(转移条件)触发状态的转移和动作的执行。动作的执行不是必须的,可以只转移状态,不指定任何动作。总体而言,状态机是一种用以表示有限个状态以及这些状态之间的转移和动作的执行等行为的数学模型。
状态机可以用公式 State(S) x Event(E) -> Actions (A), State(S’)表示,即在处于状态S的情况下,接收到了事件E,使得状态转移到了S’,同时伴随着动作A的执行。
2.2 举例
在游戏“超级马里奥”中,马里奥的形态转变就是一个状态机。马里奥有小马里奥、超级马里奥、火焰马里奥、斗篷马里奥等形态,在遇到不同的游戏情节时,会发生形态改变,同时产生积分的增减。比如小马里奥吃了蘑菇之后会变成超级马里奥,同时增加100积分。
在超级马里奥中,马里奥的不同形态就是状态机中的“状态”,游戏情节就是状态机中的“事件”,加减积分就是状态——机中的“动作”。
其中,事件E1~E4分别表示吃蘑菇、获得斗篷、获得火焰、遇到怪物。
3. 运单环节状态机实现
状态
梳理出需要监控的运单环节:揽收环节、干支运输环节、配送环节,由此派生出8个状态:初始状态、揽收开始/结束状态、干支开始/结束状态、派送开始/结束状态、结束状态。
事件、动作及状态转移
将运单状态作为事件输入到状态机中,由此触发运单环节的流转,并执行相应的动作——创建新的环节、结束当前环节、结束当前并创建下一个环节。环节的初始化和结束计算任务比较重量,采用了异步任务进行计算。
环节状态机的整体流转从揽收到干支、派送,最后到结束状态,处于结束状态的状态机不会在响应任何输入事件。如图所示,在揽收开始的状态下,接收到卸车事件,那么状态机的状态将会转变到揽收结束状态,同时执行结束环节计算任务。此外,由于干支运输和派送环节在实际中可能存在多个,所以干支开始/结束、派送开始/结束这一对状态可以互相转换。
具体实现——查表法
在代码实现上,状态机常见的三种实现方式——分支逻辑法、查表法、状态模式法,查表法比较适应当前场景:状态、事件类型很多、状态转移比较复杂,利用二维数组表示状态转移图,能极大的提高代码的可读性和可维护性。当把数组存在配置文件中,在状态机变更时甚至不需要修改代码。
此外,由于只有在状态机状态发生改变时,才需要执行动作。对ACTION_TABLE进行了优化,纵坐标由事件变化为下一个状态,即由当前状态和转移后的状态决定执行的事件类型。
/**
* 状态转移矩阵,横坐标表示当前状态,纵坐标表示接收到的运单状态变化事件,值为新状态。
*/
private static final WaybillMajorStateEnum[][] TRANSITION_TABLE = {
// S_626 S_640 S10 S_170 S_450 S_460 S_470 S_520 S16 S110 S130 S150 S133 S160 S580 S_790 S_860 S_1100 S530 S540 S630 S635 S_330 S_340 S_1890
{
INIT_STAT, LS_START , LS_FINISH, LS_FINISH, GZ_START , GZ_FINISH, GZ_FINISH, GZ_FINISH, GZ_FINISH, PS_START ,