前言
前几天和某某同学吃饭席间,他聊到每当要修改老项目中自己写的代码时就痛苦不堪,问我是不是也有同感。我觉得这应该是不少程序猿的心声,之所以会这样,大致有两个主因:
项目的整体设计很糟糕,只管往上堆砌各种功能、补丁,对于代码质量和结构关系基本无暇顾及,最终积重难返滑向失控。
对技术缺乏必要的敬畏心,基础不够扎实、知识面较窄,不能(无法)进行合理的规划,最终导致停留在低水平的代码堆砌上,只求完成功能就万事大吉。
程序猿饭桌上总少不了对产品经理的吐槽:“产品经理又对业务流程进行了疯狂调整,我觉得这会导致状态机无法支持了。”他的这个槽点让我一时有些语塞,倒不是怀疑产品经理的脑洞还能大到把状态机开到失控,只是诧异难道我们还有比状态机更适合应对业务流程变更的武器吗?
事实上状态机对于软件工程师来说应该是个很基础的知识点,它原理简单却拥有强大的适应力并被广泛应用 (譬如:游戏开发、工作流、编译器、正则表达式等解析器中) ,掌握好它的原理和应用,能帮助我们从容应对很多棘手问题,它于程序猿应对复杂流程性问题,就好比医生使用抗生素应对细菌感染一样的最佳武器。同时,它还是防止代码失控的一剂良药。
基本概念
状态机一般泛指“有限状态机(Finite State Machine)”,《离散数学》中有关于它的专门章节,以下谨为我对相关概念的形式上的非精准释义,如有出入请以教科书或相关学术资料为准。
状态:顾名思义表示某个时刻系统处于一个特定的阶段。通常我们不考虑中间态,也可以把中间态进行退化处理。当状态发生变更,就叫状态转换(Transfer)或状态迁移(Transition)。
事件:驱动系统进行状态转换/迁移的源,提供这种源的也常被称为“触发器(Trigger)”。
行为:当系统进行状态转换时进行的响应处理,提供响应处理的程序也常被称为“处理器(Handler)”。
有了上面的基本概念,我们来看一个最简单的状态图:
状态图
你可能会奇怪这个图怎么跟网上那些状态机图不一样,连状态转换条件都没有呢?这是因为,我觉得在了解状态机之前,最好先将确立以下两种概念:
状态驱动: 状态机负责根据输入来驱动状态流转。
迁移判定: 在状态流转过程中确定当前状态是否需要进行转换/迁移,以及转换/迁移到哪个状态中的判定机制。
所以,在常见的状态机图中标注的那些状态转换条件只是“迁移判定”的一种具体表现形式,它即可以由状态机内置,也可以是独立的判定器来处理,又或者由状态图预先定义好,如此等等。
建立“状态驱动”和“迁移判定”这两个被抽象化的概念,有助于我们深入理解状态机的机理,并且对我们设计一个鲁棒性和扩展性更好到状态机有实际指导意义。
状态机图
以下是表示一个‘简陋’的 Email 地址格式的解析器状态图,状态迁移条件采用正则表达式来表达,其中图二又称为“状态迁移图”。
节点式状态机图
图一:节点式
表格式状态机图
图二:表格式(红色格表示拒绝或异常;灰色格表示忽略或无意义;其他表示迁移条件)
代码实现
有了上面的状态图,就像建筑工人拿到了详细的建筑设计图纸;现在我们只需要对着状态机图,把它映射成代码即可完成一个基本状态机。状态机图越详细,实现起来就越容易,同时代码的可维护性也越好。
public class Email
{
public string Identifier { get; private set;}
public string Host { get; private set; }
public string Domain { get; private set; }
private Email() {}
public static Email Parse(string text)
{
if(string.IsNullOrEmpty(text))
return null;
var state = State.None;
/* The State-Driven */
for(int i=0; i