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

23 篇文章 0 订阅
12 篇文章 0 订阅

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 drink
        processState
            Drinking
            {}
    }
    
    • 添加具体实现类的类名

    用于实现状态机中的具体行为的类的名字。用 %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
            {}
    
        processState
            Wandering
            {}
    }
    
    Wandering
    {
        processState
            Satisfied
            {
                hangOutAWhile(ctxt.getStateDuration());
            }
    }
    
    Sleeping
    {
        processState
            Satisfied
            {
                sleepAWhile(ctxt.getStateDuration());
            }
    }
    %%
    
  • 语法

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

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

    • 简单状态迁移

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

    Thirsty
    {
        // to drink
        processState
            Drinking
            {}
    }
    
    • 条件性状态迁移

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

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

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

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

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

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

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

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

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

    • 指定进入状态时的行为

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

    Eating
        Entry
        {
            eatBread(ctxt.getBreadSupply());
        }
    {
        // still hungry after eating
        processState [context.getOwner().isHungry()]
            Starving
            {}
    
        // feel thirsty after eating
        processState [context.getOwner().isThirsty()]
            Thirsty
            {
                setThirstyFlag(true);
            }
    
        // well fed
        processState
            Satisfied
            {
                setStarvingFlag(false);
            }
    }
    

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

    • 指定退出状态时的行为

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

    Satisfied
            Exit
            {
                saySomething();
            }
    {
        // raining outside
        processState [!context.getOwner().isGoodWeather()]
            Sleeping
            {}
    
        // warm outside
        processState [context.getOwner().isGoodWeather()]
            Wandering
            {}
    
        maintenanceTime(isBlocked: bool)
            nil
            {
                startCLI(isBlocked);
            }
    }
    
    • 定义默认方法

    某些情况下,我们可能需要为某个状态提供多个函数方法。但由于生成的代码基于 状态模式 实现,每个状态要提供的方法个数和名称都必须相同。这时,我们就可以定义默认方法,作为其它不必支持多个函数方法的状态的默认实现;或者作为当我们对各状态要处理的所有场景考虑不周时的最终保障。

    新增 Default 状态,并为需要实现的默认方法指定内容。如下:

    Default
    {
        processState
            nil
            {
                doSelfCheck();
                dumpRuntimeInfo();
            }
    }
    
3.2 .sm 文件示例

了解 .sm 文件的格式及语法后,根据前文得出的状态迁移关系,编写文件 EatMealMan.sm。内容如下:

%{
//
// Copyright (c) 2020
// All rights reserved.
//
// Author: Qidi (huang_qi_di@hotmail.com)
//
%}

// This FSM works for the EatMealMan class only and
// only the EatMealMan class may instantiate it.

%class EatMealMan
%header EatMealMan.h
%fsmclass EatMealManFSM
%fsmfile EatMealManFSM

// A %map name cannot be the same as the FSM class name.
%start EatMealManMap::Satisfied
%map EatMealManMap
%%
Satisfied
{
	// raining outside
	processState [!context.getOwner().isGoodWeather()]
		Sleeping
		{}

	// warm outside
	processState [context.getOwner().isGoodWeather()]
		Wandering
		{}

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

Wandering
	Entry
	{
		// waterDemand and breadDemand is
		// the consumption happened during wandering
		consumeWater(ctxt.getWaterDemand());
		consumeBread(ctxt.getBreadDemand());
	}
{
	// happen to be thirsty
	processState [context.getOwner().isThirsty()]
        Thirsty
        {}

	// happen to be hungry
	processState [context.getOwner().isHungry()]
        Starving
        {}

	// neither thirsty nor hungry
	processState
		Satisfied
		{
			hangOutAWhile(ctxt.getStateDuration());
		}

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

Sleeping
	Entry
	{
		consumeWater(ctxt.getWaterDemand());
		consumeBread(ctxt.getBreadDemand());
	}
{
	processState [context.getOwner().isThirsty()]
        Thirsty
        {}

	processState [context.getOwner().isHungry()]
		Starving
        {}

	processState
		Satisfied
		{
			sleepAWhile(ctxt.getStateDuration());
		}

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

Thirsty
{
	// to drink
	processState
		Drinking
		{
			setThirstyFlag(true);
		}

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

Starving
{
	// to eat
	processState
		Eating
		{
			setStarvingFlag(true);
		}

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

Drinking
	Entry
	{
		drinkWater(ctxt.getWaterSupply());
	}
{
	// still thirsty after drinking
	processState [context.getOwner().isThirsty()]
		Thirsty
		{}

	// feel hungry after drinking
	processState [context.getOwner().isHungry()]
		Starving
		{
			setStarvingFlag(true);
		}

	// refilled
	processState
		Satisfied
		{
			setThirstyFlag(false);
		}

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

Eating
	Entry
	{
		eatBread(ctxt.getBreadSupply());
	}
{
	// still hungry after eating
	processState [context.getOwner().isHungry()]
		Starving
        {}

	// feel thirsty after eating
	processState [context.getOwner().isThirsty()]
		Thirsty
		{
			setThirstyFlag(true);
		}

	// well fed
	processState
		Satisfied
		{
			setStarvingFlag(false);
		}

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

Default
{
	processState
		nil
		{
			doSelfCheck();
			dumpRuntimeInfo();
		}
}
%%

4. 生成状态机代码

生成状态机代码的命令格式为:

java -jar <smc.jar文件位置> <目标语言类型> [参数] <sm文件名>

以我本地执行的具体命令为例,生成 C++ 源文件 EatMealManFSM.cpp 和头文件 EatMealManFSM.h,命令如下:

java -jar ..\Smc.jar -c++ -noex -nocatch -nostreams EatMealMan.sm

其中 -noex 表示不生成抛出异常代码,-nocatch 表示不生成捕获异常代码,-nostreams 表示不使用 iostream 库打印日志信息。
更多参数含义可以参考 官方说明(点我查看)

生成的 EatMealManFSM.cpp 内容如下:

//
// ex: set ro:
// DO NOT EDIT.
// generated by smc (http://smc.sourceforge.net/)
// from file : EatMealMan.sm
//


//
// Copyright (c) 2020
// All rights reserved.
//
// Author: Qidi (huang_qi_di@hotmail.com)
//


#include "EatMealMan.h"
#include "EatMealManFSM.h"

using namespace statemap;

// Static class declarations.
EatMealManMap_Satisfied EatMealManMap::Satisfied("EatMealManMap::Satisfied", 0);
EatMealManMap_Wandering EatMealManMap::Wandering("EatMealManMap::Wandering", 1);
EatMealManMap_Sleeping EatMealManMap::Sleeping("EatMealManMap::Sleeping", 2);
EatMealManMap_Thirsty EatMealManMap::Thirsty("EatMealManMap::Thirsty", 3);
EatMealManMap_Starving EatMealManMap::Starving("EatMealManMap::Starving", 4);
EatMealManMap_Drinking EatMealManMap::Drinking("EatMealManMap::Drinking", 5);
EatMealManMap_Eating EatMealManMap::Eating("EatMealManMap::Eating", 6);

void EatMealManState::maintenanceTime(EatMealManFSM& context, bool isBlocked)
{
    Default(context);
}

void EatMealManState::processState(EatMealManFSM& context)
{
    Default(context);
}

void EatMealManState::Default(EatMealManFSM& context)
{
    assert(false);
}

void EatMealManMap_Default::processState(EatMealManFSM& context)
{
    EatMealMan& ctxt = context.getOwner();

    EatMealManState& endState = context.getState();

    context.clearState();
    ctxt.doSelfCheck();
    ctxt.dumpRuntimeInfo();
    context.setState(endState);
}

void EatMealManMap_Satisfied::maintenanceTime(EatMealManFSM& context, bool isBlocked)
{
    EatMealMan& ctxt = context.getOwner();

    EatMealManState& endState = context.getState();

    context.clearState();
    ctxt.startCLI(isBlocked);
    context.setState(endState);
}

void EatMealManMap_Satisfied::processState(EatMealManFSM& context)
{

    if (!context.getOwner().isGoodWeather())
    {
        context.getState().Exit(context);
        // No actions.
        context.setState(EatMealManMap::Sleeping);
        context.getState().Entry(context);
    }
    else if (context.getOwner().isGoodWeather())

    {
        context.getState().Exit(context);
        // No actions.
        context.setState(EatMealManMap::Wandering);
        context.getState().Entry(context);
    }    else
    {
         EatMealManMap_Default::processState(context);
    }
}

void EatMealManMap_Wandering::Entry(EatMealManFSM& context)

{
    EatMealMan& ctxt = context.getOwner();

    ctxt.consumeWater(ctxt.getWaterDemand());
    ctxt.consumeBread(ctxt.getBreadDemand());
}

void EatMealManMap_Wandering::maintenanceTime(EatMealManFSM& context, bool isBlocked)
{
    EatMealMan& ctxt = context.getOwner();

    EatMealManState& endState = context.getState();

    context.clearState();
    ctxt.startCLI(isBlocked);
    context.setState(endState);
}

void EatMealManMap_Wandering::processState(EatMealManFSM& context)
{
    EatMealMan& ctxt = context.getOwner();

    if (context.getOwner().isThirsty())
    {
        context.getState().Exit(context);
        // No actions.
        context.setState(EatMealManMap::Thirsty);
        context.getState().Entry(context);
    }
    else if (context.getOwner().isHungry())

    {
        context.getState().Exit(context);
        // No actions.
        context.setState(EatMealManMap::Starving);
        context.getState().Entry(context);
    }
    else
    {
        context.getState().Exit(context);
        context.clearState();
        ctxt.hangOutAWhile(ctxt.getStateDuration());
        context.setState(EatMealManMap::Satisfied);
        context.getState().Entry(context);
    }
}

void EatMealManMap_Sleeping::Entry(EatMealManFSM& context)

{
    EatMealMan& ctxt = context.getOwner();

    ctxt.consumeWater(ctxt.getWaterDemand());
    ctxt.consumeBread(ctxt.getBreadDemand());
}

void EatMealManMap_Sleeping::maintenanceTime(EatMealManFSM& context, bool isBlocked)
{
    EatMealMan& ctxt = context.getOwner();

    EatMealManState& endState = context.getState();

    context.clearState();
    ctxt.startCLI(isBlocked);
    context.setState(endState);
}

void EatMealManMap_Sleeping::processState(EatMealManFSM& context)
{
    EatMealMan& ctxt = context.getOwner();

    if (context.getOwner().isThirsty())
    {
        context.getState().Exit(context);
        // No actions.
        context.setState(EatMealManMap::Thirsty);
        context.getState().Entry(context);
    }
    else if (context.getOwner().isHungry())
    {
        context.getState().Exit(context);
        // No actions.
        context.setState(EatMealManMap::Starving);
        context.getState().Entry(context);
    }
    else
    {
        context.getState().Exit(context);
        context.clearState();
        ctxt.sleepAWhile(ctxt.getStateDuration());
        context.setState(EatMealManMap::Satisfied);
        context.getState().Entry(context);
    }
}

void EatMealManMap_Thirsty::maintenanceTime(EatMealManFSM& context, bool isBlocked)
{
    EatMealMan& ctxt = context.getOwner();

    EatMealManState& endState = context.getState();

    context.clearState();
    ctxt.startCLI(isBlocked);
    context.setState(endState);
}

void EatMealManMap_Thirsty::processState(EatMealManFSM& context)
{
    EatMealMan& ctxt = context.getOwner();

    context.getState().Exit(context);
    context.clearState();
    ctxt.setThirstyFlag(true);
    context.setState(EatMealManMap::Drinking);
    context.getState().Entry(context);
}

void EatMealManMap_Starving::maintenanceTime(EatMealManFSM& context, bool isBlocked)
{
    EatMealMan& ctxt = context.getOwner();

    EatMealManState& endState = context.getState();

    context.clearState();
    ctxt.startCLI(isBlocked);
    context.setState(endState);
}

void EatMealManMap_Starving::processState(EatMealManFSM& context)
{
    EatMealMan& ctxt = context.getOwner();

    context.getState().Exit(context);
    context.clearState();
    ctxt.setStarvingFlag(true);
    context.setState(EatMealManMap::Eating);
    context.getState().Entry(context);
}

void EatMealManMap_Drinking::Entry(EatMealManFSM& context)

{
    EatMealMan& ctxt = context.getOwner();

    ctxt.drinkWater(ctxt.getWaterSupply());
}

void EatMealManMap_Drinking::maintenanceTime(EatMealManFSM& context, bool isBlocked)
{
    EatMealMan& ctxt = context.getOwner();

    EatMealManState& endState = context.getState();

    context.clearState();
    ctxt.startCLI(isBlocked);
    context.setState(endState);
}

void EatMealManMap_Drinking::processState(EatMealManFSM& context)
{
    EatMealMan& ctxt = context.getOwner();

    if (context.getOwner().isThirsty())
    {
        context.getState().Exit(context);
        // No actions.
        context.setState(EatMealManMap::Thirsty);
        context.getState().Entry(context);
    }
    else if (context.getOwner().isHungry())
    {
        context.getState().Exit(context);
        context.clearState();
        ctxt.setStarvingFlag(true);
        context.setState(EatMealManMap::Starving);
        context.getState().Entry(context);
    }
    else
    {
        context.getState().Exit(context);
        context.clearState();
        ctxt.setThirstyFlag(false);
        context.setState(EatMealManMap::Satisfied);
        context.getState().Entry(context);
    }
}

void EatMealManMap_Eating::Entry(EatMealManFSM& context)

{
    EatMealMan& ctxt = context.getOwner();

    ctxt.eatBread(ctxt.getBreadSupply());
}

void EatMealManMap_Eating::maintenanceTime(EatMealManFSM& context, bool isBlocked)
{
    EatMealMan& ctxt = context.getOwner();

    EatMealManState& endState = context.getState();

    context.clearState();
    ctxt.startCLI(isBlocked);
    context.setState(endState);
}

void EatMealManMap_Eating::processState(EatMealManFSM& context)
{
    EatMealMan& ctxt = context.getOwner();

    if (context.getOwner().isHungry())
    {
        context.getState().Exit(context);
        // No actions.
        context.setState(EatMealManMap::Starving);
        context.getState().Entry(context);
    }
    else if (context.getOwner().isThirsty())
    {
        context.getState().Exit(context);
        context.clearState();
        ctxt.setThirstyFlag(true);
        context.setState(EatMealManMap::Thirsty);
        context.getState().Entry(context);
    }
    else
    {
        context.getState().Exit(context);
        context.clearState();
        ctxt.setStarvingFlag(false);
        context.setState(EatMealManMap::Satisfied);
        context.getState().Entry(context);
    }
}

//
// Local variables:
//  buffer-read-only: t
// End:
//

生成的 EatMealManFSM.h 内容如下:

//
// ex: set ro:
// DO NOT EDIT.
// generated by smc (http://smc.sourceforge.net/)
// from file : EatMealMan.sm
//

#ifndef EATMEALMANFSM_H
#define EATMEALMANFSM_H


#define SMC_NO_EXCEPTIONS

#include <statemap.h>

// Forward declarations.
class EatMealManMap;
class EatMealManMap_Satisfied;
class EatMealManMap_Wandering;
class EatMealManMap_Sleeping;
class EatMealManMap_Thirsty;
class EatMealManMap_Starving;
class EatMealManMap_Drinking;
class EatMealManMap_Eating;
class EatMealManMap_Default;
class EatMealManState;
class EatMealManFSM;
class EatMealMan;

class EatMealManState :
    public statemap::State
{
public:

    EatMealManState(const char * const name, const int stateId)
    : statemap::State(name, stateId)
    {};

    virtual void Entry(EatMealManFSM&) {};
    virtual void Exit(EatMealManFSM&) {};

    virtual void maintenanceTime(EatMealManFSM& context, bool isBlocked);
    virtual void processState(EatMealManFSM& context);

protected:

    virtual void Default(EatMealManFSM& context);
};

class EatMealManMap
{
public:

    static EatMealManMap_Satisfied Satisfied;
    static EatMealManMap_Wandering Wandering;
    static EatMealManMap_Sleeping Sleeping;
    static EatMealManMap_Thirsty Thirsty;
    static EatMealManMap_Starving Starving;
    static EatMealManMap_Drinking Drinking;
    static EatMealManMap_Eating Eating;
};

class EatMealManMap_Default :
    public EatMealManState
{
public:

    EatMealManMap_Default(const char * const name, const int stateId)
    : EatMealManState(name, stateId)
    {};

    virtual void processState(EatMealManFSM& context);
};

class EatMealManMap_Satisfied :
    public EatMealManMap_Default
{
public:
    EatMealManMap_Satisfied(const char * const name, const int stateId)
    : EatMealManMap_Default(name, stateId)
    {};

    virtual void maintenanceTime(EatMealManFSM& context, bool isBlocked);
    virtual void processState(EatMealManFSM& context);
};

class EatMealManMap_Wandering :
    public EatMealManMap_Default
{
public:
    EatMealManMap_Wandering(const char * const name, const int stateId)
    : EatMealManMap_Default(name, stateId)
    {};

    virtual void Entry(EatMealManFSM&);
    virtual void maintenanceTime(EatMealManFSM& context, bool isBlocked);
    virtual void processState(EatMealManFSM& context);
};

class EatMealManMap_Sleeping :
    public EatMealManMap_Default
{
public:
    EatMealManMap_Sleeping(const char * const name, const int stateId)
    : EatMealManMap_Default(name, stateId)
    {};

    virtual void Entry(EatMealManFSM&);
    virtual void maintenanceTime(EatMealManFSM& context, bool isBlocked);
    virtual void processState(EatMealManFSM& context);
};

class EatMealManMap_Thirsty :
    public EatMealManMap_Default
{
public:
    EatMealManMap_Thirsty(const char * const name, const int stateId)
    : EatMealManMap_Default(name, stateId)
    {};

    virtual void maintenanceTime(EatMealManFSM& context, bool isBlocked);
    virtual void processState(EatMealManFSM& context);
};

class EatMealManMap_Starving :
    public EatMealManMap_Default
{
public:
    EatMealManMap_Starving(const char * const name, const int stateId)
    : EatMealManMap_Default(name, stateId)
    {};

    virtual void maintenanceTime(EatMealManFSM& context, bool isBlocked);
    virtual void processState(EatMealManFSM& context);
};

class EatMealManMap_Drinking :
    public EatMealManMap_Default
{
public:
    EatMealManMap_Drinking(const char * const name, const int stateId)
    : EatMealManMap_Default(name, stateId)
    {};

    virtual void Entry(EatMealManFSM&);
    virtual void maintenanceTime(EatMealManFSM& context, bool isBlocked);
    virtual void processState(EatMealManFSM& context);
};

class EatMealManMap_Eating :
    public EatMealManMap_Default
{
public:
    EatMealManMap_Eating(const char * const name, const int stateId)
    : EatMealManMap_Default(name, stateId)
    {};

    virtual void Entry(EatMealManFSM&);
    virtual void maintenanceTime(EatMealManFSM& context, bool isBlocked);
    virtual void processState(EatMealManFSM& context);
};

class EatMealManFSM :
    public statemap::FSMContext
{
public:

    explicit EatMealManFSM(EatMealMan& owner)
    : FSMContext(EatMealManMap::Satisfied),
      _owner(owner)
    {};

    EatMealManFSM(EatMealMan& owner, const statemap::State& state)
    : FSMContext(state),
      _owner(owner)
    {};

    virtual void enterStartState()
    {
        getState().Entry(*this);
        return;
    }

    inline EatMealMan& getOwner()
    {
        return (_owner);
    };

    inline EatMealManState& getState()
    {
        assert(_state != NULL);

        return dynamic_cast<EatMealManState&>(*_state);
    };

    inline void maintenanceTime(bool isBlocked)
    {
        getState().maintenanceTime(*this, isBlocked);
    };

    inline void processState()
    {
        getState().processState(*this);
    };

private:
    EatMealMan& _owner;
};


#endif // EATMEALMANFSM_H

//
// Local variables:
//  buffer-read-only: t
// End:
//

5. 生成状态迁移图

要想生成状态迁移图,需要先生成 .dot 文件。命令格式为:

java -jar <smc.jar文件位置> -graph [参数] <sm文件名>

以我本地执行的具体命令为例,生成 .dot 文件 EatMealMan.dot,命令如下:

java -jar ..\Smc.jar -graph -glevel 1 EatMealMan.sm

再使用 graphviz 工具集(点我下载安装)中的 dot 命令,生成状态迁移图:

dot -Tpng EatMealMan.dot -o EatMealManMap.png

其中,-Txxx 参数用于指定生成的图片格式,-o 参数用于指定生成的图片文件名。
有关 graphviz 的安装和详细使用方式,网上也有很多。本文不作说明,请读者朋友们自行检索。

自动生成的状态迁移图如下:
自动状态

和手绘的相比,自动生成的迁移图丑是丑了点,但状态间的关系是一致的。


6. 基于生成的状态机代码实现具体功能

SMC 工具为我们生成的代码只能管理状态转换关系,各状态中被调用的函数的具体行为仍然要我们自己实现 —— 也就是要实现上文中出现的 isGoodWeather()isHungry()sleepAWhile()consumeWater() 等函数的函数体。

EatMealMan.sm 中已经指定了具体实现类的类名和头文件名分别为 EatMealManEatMealMan.h。于是我们创建源文件 EatMealMan.cpp 和头文件 EatMealMan.h

EatMealMan.cpp 内容如下(用于说明目的,并未完整实现):

#include "EatMealMan.h"

EatMealMan::EatMealMan()
{
	_fsm = new EatMealManFSM();
}

EatMealMan::~EatMealMan() {}

bool EatMealMan::isGoodWeather()
{
	return mWeatherState;
}

bool EatMealMan::isThirsty()
{
	float waterShortage = mMaxWater - mWaterStorage;
	if (waterShortage < 0.1)
		return true;
	return false;
}

bool EatMealMan::isHungry()
{
	float breadShortage = mMaxBread - mBreadStorage;
	if (breadShortage < 1)
		return true;
	return false;
}

float EatMealMan::getWaterDemand() {}
float EatMealMan::getBreadDemand() {}
void EatMealMan::consumeWater(float waterVolume) {}
void EatMealMan::consumeBread(float breadNum) {}

void EatMealMan::hangOutAWhile(long durationSeconds) {}
void EatMealMan::sleepAWhile(long durationSeconds) {}
long EatMealMan::getStateDuration() {}

void EatMealMan::setThirstyFlag(bool val)
{
	mThirstyState = val;
}
void EatMealMan::setStarvingFlag(bool val)
{
	mStarvingState = val;
}

float EatMealMan::getBreadSupply() {}
float EatMealMan::getWaterSupply() {}
void EatMealMan::eatBread(float breadNum) {}
void EatMealMan::drinkWater(float waterVolume) {}

void EatMealMan::doSelfCheck() {}
void EatMealMan::dumpRuntimeInfo() {}
void EatMealMan::startCLI(bool isBlocked) {}

EatMealMan.h 内容如下:

#ifndef __EATMEALMAN_H__
#define __EATMEALMAN_H__

Class EatMealMan {
	bool mWeatherState;
	bool mStarvingState;
	bool mThirstyState;
	float mWaterStorage;
	float mBreadStorage;
	const float mMaxBread = 10.0;
	const float mMaxWater = 2.0;

	EatMealManFSM _fsm;

public:
	bool isGoodWeather();
	bool isThirsty();
	bool isHungry();

	float getWaterDemand();
	float getBreadDemand();
	void consumeWater(float waterVolume);
	void consumeBread(float breadNum);

	void hangOutAWhile(long durationSeconds);
	void sleepAWhile(long durationSeconds);
	long getStateDuration();

	void setThirstyFlag(bool val);
	void setStarvingFlag(bool val);

	float getBreadSupply();
	float getWaterSupply();
	void eatBread(float breadNum);
	void drinkWater(float waterVolume);

	void doSelfCheck();
	void dumpRuntimeInfo();
	void startCLI(bool isBlocked);
};
#endif

这里有三点需要注意。

其一是在 EatMealMan.h 中引用了状态机类的头文件 EatMealManFSM.h,并添加了状态机成员 EatMealManFSM _fsm

#ifndef __EATMEALMAN_H__
#define __EATMEALMAN_H__

#include "EatMealManFSM.h"

Class EatMealMan {
	//... 省略
	EatMealManFSM _fsm;

public:
	//... 省略
};
#endif

其二是在具体实现类的构造函数中实例化了状态机对象,并使状态机进入到初始状态:

#include "EatMealMan.h"

EatMealMan::EatMealMan()
{
	_fsm = new EatMealManFSM();
    _fsm.enterStartState();  // 这会调用当前状态的 Entry() 方法
}

EatMealMan::~EatMealMan() {}
//... 省略

其三是需要把头文件 statemap.h 加入到编译环境中。SMC 的开发者在 SourceForge.net 上为不同的编程语言提供了不同版本的头文件,我们需要根据自己要生成的编程语言类型选择合适的版本进行下载(点我下载 C++ 版本头文件)


至此,我们有了状态机类 EatMealManFSM,也有了具体实现类 EatMealMan,并且将它们关联在了一起。所有准备工作都已完成,接下来我们只需要在要处理具体状态的地方调用 EatMealManFSM::processState() 方法就可以了。比如:

EatMealMan::threadLoop()
{
	_fsm.processState();
}

8. 参考文献

[1] SMC - Preface
[2] SMC - The State Machine Compiler

评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值