State Machine Compiler 用法:使用 SMC 工具自动生成状态机代码

State Machine Compiler 用法:使用 SMC 工具自动生成状态机代码


Qidi 2020.12.18 (MarkDown & EnterpriseArchitect & Haroopad)


0. SMC 简介

SMC (State Machine Compiler) 目前还没有正式中文名,这里把它意译成 状态机生成器,其主要作者是 Charles W. Rapp。状态机生成器实际上早在 2000 年左右就由 Robert C. Martin (他还有个网名叫 Uncle Bob) 创造出来了,现在一般把他发明的状态机生成器称作 OSMC (Original State Machine Compiler)。Charles 曾经是 Robert 的同事,在后者离职后,前者接手了 OSMC 的维护工作[1]

本文介绍的 SMC 使用 Java 写成,在 OSMC 的基础上进行了扩展和优化,可用于生成基于 状态模式(State Pattern) 实现的状态机代码,并且支持多达 14 种编程语言[2]。相关源码可以在 SourceForge.net 上查看。如果对 SMC 的代码不感兴趣,也可以直接下载最新版本的 jar 包使用(点我下载)。


1. 为 SMC 配置运行环境

下载并解压后,得到可执行文件 Smc.jar,但需要先安装 Java 运行环境才能运行它。
安装 JRE 的教程网上很多,读者可以自行搜索,或者参考《JRE的安装及环境变量配置》这篇文章进行操作。


2. 选定目标场景,确定状态迁移关系

比如我们对 干饭人 混吃等死的美好生活很感兴趣,想实现一个表现 干饭人的一天 的状态机。经过一段时间遐想后,可以得出和干饭人直接相关的状态有:

  • 饿了
  • 渴了
  • 吃饭
  • 喝水
  • 闲逛
  • 睡觉
  • 满足

其中 满足 可以作为干饭人的起始状态,视天气而定,转换到 闲逛 或者 睡觉 状态,在这两种状态下,随时间流逝又可以转换为 饿了 或者 渴了 状态,继而转换到 吃饭 或者 喝水 状态。吃掉一定量的食物后(喝水同理),干饭人有可能还没吃饱,这时就会回到 饿了 的状态;或者有可能已经吃饱但又口渴了,这时就会转换到 渴了 的状态;如果此时既不饿也不渴,就会转回到 满足 的状态,实现一次循环。

如此,我们就可以手绘出一幅状态迁移图,如下所示:
手绘状态


3. 编写 .sm 文件

理清状态迁移关系之后,为了下一步使用 SMC 工具自动生成代码,我们就可以开始编写 .sm 文件了。

3.1 .sm 文件的格式和语法

关于 .sm 文件的格式和语法,详细说明可以参考 官方文档(点我阅读)。本文只对基础用法进行说明,搭配 3.2节 中的 示例文件 进行阅读,效果更佳。

  • 格式

    • 添加源文件版权声明/免责声明

    使用 %{...}% 将声明内容括起来进行标识,比如:

    %{
    // put disclaimer or copy right here
    %}
    
    • 添加注释

    使用 // 进行标识,比如:

    Thirsty
    {// to drinkprocessStateDrinking{}
    }
    
    • 添加具体实现类的类名

    用于实现状态机中的具体行为的类的名字。用 %class 关键字来标识,比如:

    %class EatMealMan
    
    • 添加声明具体实现类的头文件名

    包含具体实现类的类声明的头文件的名字。用 %header 关键字来标识,比如:

    %header EatMealMan.h
    
    • 指定要生成的状态机类的类名

    要生成的状态机类的名字。用 %fsmclass 关键字来标识,比如:

    %fsmclass EatMealManFSM
    
    • 指定要生成的状态机类所在的源文件名

    要生成的包含状态机类实现的源文件名(不含扩展文件名)。用 %fsmfile 关键字来标识,比如:

    %fsmfile EatMealManFSM
    
    • 指定状态机起始状态

    使用 %start 关键字来标识,比如:

    %start EatMealManMap::Satisfied
    
    • 指定状态迁移图名称

    使用 %map 关键字来标识,比如:

    %map EatMealManMap
    
    • 添加各状态定义及迁移关系

    使用 %%...%% 将各状态及其迁移关系扩起来进行标识,比如:

    %%
    Satisfied
    {processState [!context.getOwner().isGoodWeather()]Sleeping{}processStateWandering{}
    }Wandering
    {processStateSatisfied{hangOutAWhile(ctxt.getStateDuration());}
    }Sleeping
    {processStateSatisfied{sleepAWhile(ctxt.getStateDuration());}
    }
    %%
    
  • 语法

    • 语法总览
    StateNameEntry{// 在这里添加进入 StateName 状态时的行为}Exit{// 在这里添加退出 StateName 状态时的行为}
    {TriggerMethod [triggerConditions]NewStateName{// 在这里添加转换到 NewStateName 状态前的行为}
    }
    

    其中 StateName 是当前的状态名,NewStateName 是要切换到的状态名;
    EntryExit 及它们的行为定义都是可选的,如果没有需要可以不写;
    TriggerMethod 是触发状态转换的方法名,triggerCondition 是转换到 NewStateName 状态需要满足的条件。triggerCondition 也是可选的。

    • 简单状态迁移

    只需定义当前状态、目标状态、触发方法。如下:

    Thirsty
    {// to drinkprocessStateDrinking{}
    }
    
    • 条件性状态迁移

    在简单状态迁移写法的基础上,在 TriggerMethod 后,使用 方括号[] 添加状态转换的保护条件(Guard Condition)。只有当条件满足时,才会进行状态切换。如下:

    Sleeping
    {processState [context.getOwner().isThirsty()]Thirsty{}processState [context.getOwner().isHungry()]Starving{}processStateSatisfied{}
    }
    

    在多个相同的 TriggerName 后添加不同的保护条件和目标状态,其含义为:在该函数中,满足不同条件时分别切换到不同的状态。

    • 指定转换到新状态前的行为

    在目标状态的花括号中添加的行为,会在转换到目标状态前被执行。如下:

    Thirsty
    {// to drinkprocessStateDrinking{setThirstyFlag(true);}
    }
    
    • 带参数的状态迁移

    TriggerMethod 后,使用 圆括号() 添加触发函数的参数。如下:

    Thirsty
    {maintenanceTime(isBlocked: bool)nil{startCLI(isBlocked);}
    }
    

    其中 maintenanceTime 是触发函数的名字,isBlocked 是函数参数,bool 是参数类型。这个函数的作用是使干饭人进入到维护模式,以便开发人员实时检查运行状态。 nil 表示这个函数不会触发状态切换,状态机会将当前状态设置为最终状态。

    • 指定进入状态时的行为

    使用 Entry 关键字来指定进入状态时的行为。如下:

    EatingEntry{eatBread(ctxt.getBreadSupply());}
    {// still hungry after eatingprocessState [context.getOwner().isHungry()]Starving{}// feel thirsty after eatingprocessState [context.getOwner().isThirsty()]Thirsty{setThirstyFlag(true);}// well fedprocessStateSatisfied{setStarvingFlag(false);}
    }
    

    这个示例表示:进入 Eating 状态时,就执行 eatBread() 函数。

    • 指定退出状态时的行为

    类似的,使用 Exit 关键字来指定退出状态时的行为。如下:

    SatisfiedExit{saySomething();}
    {// raining outsideprocessState [!context.getOwner().isGoodWeat


本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!

相关文章

立即
投稿

微信公众账号

微信扫一扫加关注

返回
顶部