使用C++实现一套简单的状态机模型——实例

        一般来说,“状态机”是一种表达状态转换变换逻辑的方法。曾经有人和我讨论过为什么不直接用ifelse,而要使用“状态机”去实现一些逻辑,认为使用“状态机”是一种炫技的表现。然而对于大型复杂逻辑的变化和跳转,使用ifelse将带来代码难以阅读等弊端。其实ifelse也是一种状态机实现的方式。

        之前我们有个业务和操作系统有着强烈的关联,而我们希望比较清晰地描述整个业务中各个子业务的过程,就引入了状态机描述的方式。可是当时的状态机是使用if else方法描述,显得整个过程比较臃肿,阅读起来也不够清晰。于是我尝试引入第三方的状态机库来重构这块的业务——比如boost里的状态机库。可是使用过程中感觉到了很多不便,索性自己动手实现一套清晰优雅的状态机模型。(转载请指明出于breaksoftware的csdn博客)

       编写模型之前,我们需要了解什么是状态机。我在搜索引擎上搜索到了若干结果,但是大部分都显得非常学术化。而实现一个大而全、包罗万象、放之四海而皆适宜的状态机模型也并非我的设计初衷。我设计的状态机具有如下特性:单线程、浅历史。单线程即我们的状态机是在一个线程内部运行的,不受外界其他线程干扰,这样我们在设计时就不用考虑多线程编程的问题。浅历史是状态机中的一个概念,它是指只记录最高一层复合状态的最后离开状态。这个特性如果有不了解的,可以先去搜索下。在实践中,该特性还是非常有用的。

        我们以一个简单、可能不恰当的例子来引入我这个状态机。我们先设计一个应用场景:给用户电脑安装软件并运行。这个场景我们可以拆分为如下几个逻辑:

  1. 检测是否安装
  2. 下载安装包
  3. 解压安装包并安装
  4. 运行

        这四个逻辑并不复杂,我们将其定义为基础状态——一种可以持续一段时间且内部执行逻辑我们不关心的状态。为了让这个逻辑变得稍微有点复杂,我们设计如下要求:

        对于未安装该软件的情况:

  • 从A地址下载安装包失败后从B地址下载
  • 从B地址下载安装包失败后从C地址下载
  • 从C地址下载安装包失败后认为执行失败
  • 下载成功后,检测CPU是否繁忙
  • CPU繁忙则继续检测CPU是否繁忙
  • CPU不繁忙则执行解压
  • 解压失败则重新下载。如果之前后A地址下载,则本次从B地址下载;如果之前从B地址下载,则本次从C地址下载
  • 解压成功后执行
  • 运行失败则重新下载。如果之前后A地址下载,则本次从B地址下载;如果之前从B地址下载,则本次从C地址下载
  • 运行成功则认为执行成功

        对于已安装该软件的情况:

  • 运行失败则先进行卸载,然后进入“未安装该软件”逻辑
  • 运行成功则认为执行成功

        我们以状态图来表示:

        图中“下载复合状态”是一个具有浅历史特性的复合状态;“安装后运行状态”是一个状态组合集,它让一组复杂的状态转换关系缩变成一种状态。这样如果其他地方需要复用该组合时,只要引入该组合状态名即可。

        我们从该模型使用者的角度去看如何去设计和编写代码,至于代码中的模板和函数可以先忽略掉,我们先了解其大概使用。

        从上图中我们可以确定有如下输出条件

/* CondDefine.h
*/
#pragma once

// 是否安装
#define CONDITION_EXIST "CONDITION_EXIST"
#define CONDITION_NOEXIST "CONDITION_NOEXIST"

// 下载是否成功
#define CONDITION_DOWNLOAD_SUC "CONDITION_DONWLOAD_SUC"
#define CONDITION_DOWNLOAD_FAI "CONDITION_DONWLOAD_FAI"

// CPU是否繁忙
#define CONDITION_BUSY "CONDITION_BUSY"
#define CONDITION_NOBUSY "CONDITION_NOBUSY"

// 解压是否成功
#define CONIDTION_UNZIP_SUC "CONIDTION_UNZIP_SUC"
#define CONDITION_UNZIP_FAI "CONDITION_UNZIP_FAI"

// 运行是否成功
#define CONDITION_RUN_SUC "CONDITION_RUN_SUC"
#define CONDITION_RUN_FAI "CONDITION_RUN_FAI"

// 安装是否成功
#define CONDITION_INSTALL_SUC "CONDITION_INSTALL_SUC"
#define CONDITION_INSTALL_FAI "CONDITION_INSTALL_FAI"

        整个状态跳转具有如下基础状态

基础状态
检测是否安装CSimpleState_CheckExist
从A地址下载CSimpleState_Download_From_A
从B地址下载CSimpleState_Download_From_B
从C地址下载CSimpleState_Download_From_C
检测CPU占用率CSimpleState_CheckCPU
解压CSimpleState_Unzip
安装CSimpleState_Install
卸载CSimpleState_Uninstall
执行成功CSimpleState_Success
执行失败CSimpleState_Failed
运行CSimpleState_Run

        我们以“从A地址下载”为例,查看该状态的基础代码

#pragma once
#include "AutoStateChart.h"
#include "StoreMachine.h"

// 引入输出宏
#include "CondDefine.h"

// 引入业务基类
#include "Business_Random.h"

class CMachine_Download_Run_App;     // 前置申明状态机类

class CSimpleState_Download_From_A :
    public AutoStateChart::CAutoStateChartBase<CSimpleState_Download_From_A, CMachine_Download_Run_App, CStoreofMachine>
{
public:
    CSimpleState_Download_From_A(void) {};
    ~CSimpleState_Download_From_A(void){};
public:
    void Entry() {
    };

    std::string Exit() {
        return CONDITION_DOWNLOAD_SUC;
    };
};

        可以发现,该类非常简单。我们只要用模板申明好类(模板参数:自己、状态机类、存储类),并实现Entry和Exit两个函数就行了。我们再看下下载的复合状态类

#pragma once
#include "AutoStateChart.h"
#include "StoreMachine.h"

// 引入输出宏
#include "CondDefine.h"

// 引入子状态
#include "SimpleState_Download_From_A.h"
#include "SimpleState_Download_From_B.h"
#include "SimpleState_Download_From_C.h"

class CMachine_Download_Run_App;     // 前置申明状态机类

// 该类将产生两种输出CONDITION_DONWLOAD_SUC、CONDITION_DONWLOAD_FAI
class CCompositeState_Download:
    public AutoStateChart::CCompositeStates<CCompositeState_Download, CMachine_Download_Run_App, CStoreofMachine>
{
public:
    CCompositeState_Download(void) {};
    ~CCompositeState_Download(void) {};
public:
    REGISTERSTATECONVERTBEGIN(CSimpleState_Download_From_A)
    REGISTERSTATECONVERT(CSimpleState_Download_From_A, CONDITION_DOWNLOAD_FAI, CSimpleState_Download_From_B)
    REGISTERSTATECONVERT(CSimpleState_Download_From_B, CONDITION_DOWNLOAD_FAI, CSimpleState_Download_From_C)
    REGISTERSTATECONVERTEND()
};

        这个类也非常简单,它对应于图中的


        其中REGISTERSTATECONVERTBEGIN宏指定了该复合状态的起始状态(状态类),REGISTERSTATECONVERT指定了状态翻转逻辑(前状态类,条件,后状态类)

        我们再看下“安装后运行状态”这个组合状态的类

#pragma once
#include "AutoStateChart.h"
#include "StoreMachine.h"

// 引入输出宏
#include "CondDefine.h"

// 引入子状态
#include "CompositeState_Download.h"
#include "SimpleState_Failed.h"
#include "SimpleState_CheckCPU.h"
#include "SimpleState_Unzip.h"
#include "SimpleState_Install.h"
#include "SimpleState_Run.h"
#include "SimpleState_Uninstall.h"
#include "SimpleState_Success.h"

class CMachine_Download_Run_App;     // 前置申明状态机类

class CCollectionState_Install_Run:
    public AutoStateChart::CCollectionStates<CCollectionState_Install_Run, CMachine_Download_Run_App, CStoreofMachine>
{
public:
    CCollectionState_Install_Run(void){};
    ~CCollectionState_Install_Run(void){};
public:
    REGISTERSTATECONVERTBEGIN(CCompositeState_Download)
    REGISTERSTATECONVERT(CCompositeState_Download, CONDITION_DOWNLOAD_SUC, CSimpleState_CheckCPU)
    REGISTERSTATECONVERT(CCompositeState_Download, CONDITION_DOWNLOAD_FAI, CSimpleState_Failed)

    REGISTERSTATECONVERT(CSimpleState_CheckCPU, CONDITION_BUSY, CSimpleState_CheckCPU)
    REGISTERSTATECONVERT(CSimpleState_CheckCPU, CONDITION_NOBUSY, CSimpleState_Unzip)

    REGISTERSTATECONVERT(CSimpleState_Unzip, CONIDTION_UNZIP_SUC, CSimpleState_Install)
    REGISTERSTATECONVERT(CSimpleState_Unzip, CONDITION_UNZIP_FAI, CCompositeState_Download)

    REGISTERSTATECONVERT(CSimpleState_Install, CONDITION_INSTALL_SUC, CSimpleState_Run)
    REGISTERSTATECONVERT(CSimpleState_Install, CONDITION_INSTALL_FAI, CCompositeState_Download)

    REGISTERSTATECONVERT(CSimpleState_Run, CONDITION_RUN_SUC, CSimpleState_Success)
    REGISTERSTATECONVERT(CSimpleState_Run, CONDITION_RUN_FAI, CSimpleState_Uninstall)

    REGISTERSTATECONVERT(CSimpleState_Uninstall, "", CCompositeState_Download)

    REGISTERSTATECONVERTEND()
};

        该类的写法也很简单REGISTERSTATECONVERTBEGIN、REGISTERSTATECONVERT和REGISTERSTATECONVERTEND三个宏构成了整个状态跳转图


        最后我们再看下状态机的类

#pragma once
#include "AutoStateChart.h"
#include "StoreMachine.h"

// 引入输出宏
#include "CondDefine.h"

// 引入子状态
#include "SimpleState_CheckExist.h"
#include "CollectionState_Install_Run.h"
#include "SimpleState_Run.h"
#include "SimpleState_Uninstall.h"
#include "SimpleState_Success.h"

class CMachine_Download_Run_App :
    public AutoStateChart::CAutoStateChartMachine<CMachine_Download_Run_App, CStoreofMachine>
{
public:
    CMachine_Download_Run_App(void) {};
    ~CMachine_Download_Run_App(void) {};
public:
    REGISTERSTATECONVERTBEGIN(CSimpleState_CheckExist)
    REGISTERSTATECONVERT(CSimpleState_CheckExist, CONDITION_NOEXIST, CCollectionState_Install_Run)
    REGISTERSTATECONVERT(CSimpleState_CheckExist, CONDITION_EXIST, CSimpleState_Run)

    REGISTERSTATECONVERT(CSimpleState_Run, CONDITION_RUN_FAI, CSimpleState_Uninstall)
    REGISTERSTATECONVERT(CSimpleState_Run, CONDITION_RUN_SUC, CSimpleState_Success)

    REGISTERSTATECONVERT(CSimpleState_Uninstall, "", CCollectionState_Install_Run)

    REGISTERSTATECONVERTEND()
};

        它也是通过三个宏构成了整个逻辑跳转。

        在模块独立的前提下,该状态机还算是比较优雅简洁的展现了整个状态跳转的流程。当然在这个简洁的背后还是隐藏了很多背后的秘密。我们将在下节介绍其实现。

        我们最终通过如下代码,让整个状态机运行起来:

    boost::shared_ptr<CMachine_Download_Run_App> spc = boost::make_shared<CMachine_Download_Run_App>();
    spc->StartMachine();

 

  • 8
    点赞
  • 39
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
多维状态空间模型是一种常用的时序数据建模方法,可以用于预测、滤波、参数估计等多个方面。下面我们就使用C++带类实现一个多维状态空间模型,并给出一个案例。 首先,我们需要定义一个StateSpace类,其中包括以下成员变量和成员函数: 1.成员变量 - n_:状态向量的维度 - m_:观测向量的维度 - A_:状态转移矩阵 - B_:控制向量系数矩阵 - C_:观测向量系数矩阵 - Q_:状态噪声协方差矩阵 - R_:观测噪声协方差矩阵 - x_:当前状态向量 - P_:当前状态协方差矩阵 2.成员函数 - StateSpace(int n, int m):构造函数,初始化n_和m_,并分配内存空间。 - ~StateSpace():析构函数,释放内存空间。 - void setA(Mat &A):设置状态转移矩阵A_。 - void setB(Mat &B):设置控制向量系数矩阵B_。 - void setC(Mat &C):设置观测向量系数矩阵C_。 - void setQ(Mat &Q):设置状态噪声协方差矩阵Q_。 - void setR(Mat &R):设置观测噪声协方差矩阵R_。 - void setState(Vec &x):设置当前状态向量x_。 - void setP(Mat &P):设置当前状态协方差矩阵P_。 - Vec getState() const:获取当前状态向量x_。 - Mat getP() const:获取当前状态协方差矩阵P_。 - void update(Vec &u, Vec &y):根据控制向量u和观测向量y更新状态向量x_和状态协方差矩阵P_。 其中,Mat和Vec分别表示矩阵和向量,可以使用OpenCV库中的Mat和Mat_<T>类来实现。 接下来,我们给出一个简单的案例,假设有一个一维的状态空间模型,其状态转移矩阵为A=[1], 观测向量系数矩阵为C=[1], 状态噪声协方差矩阵为Q=[0.1], 观测噪声协方差矩阵为R=[1]。初始状态向量为x_0=[0],初始状态协方差矩阵为P_0=[1]。我们将使用模型预测未来5个时刻的状态。 代码实现如下: ```cpp #include <iostream> #include <opencv2/opencv.hpp> using namespace std; using namespace cv; class StateSpace { public: StateSpace(int n, int m); ~StateSpace(); void setA(Mat &A); void setB(Mat &B); void setC(Mat &C); void setQ(Mat &Q); void setR(Mat &R); void setState(Vec &x); void setP(Mat &P); Vec getState() const; Mat getP() const; void update(Vec &u, Vec &y); private: int n_; //状态向量的维度 int m_; //观测向量的维度 Mat A_; //状态转移矩阵 Mat B_; //控制向量系数矩阵 Mat C_; //观测向量系数矩阵 Mat Q_; //状态噪声协方差矩阵 Mat R_; //观测噪声协方差矩阵 Vec x_; //当前状态向量 Mat P_; //当前状态协方差矩阵 }; StateSpace::StateSpace(int n, int m) { n_ = n; m_ = m; A_ = Mat::zeros(n_, n_, CV_64FC1); B_ = Mat::zeros(n_, 1, CV_64FC1); C_ = Mat::zeros(m_, n_, CV_64FC1); Q_ = Mat::zeros(n_, n_, CV_64FC1); R_ = Mat::zeros(m_, m_, CV_64FC1); x_ = Vec::zeros(n_); P_ = Mat::zeros(n_, n_, CV_64FC1); } StateSpace::~StateSpace() { } void StateSpace::setA(Mat &A) { A.copyTo(A_); } void StateSpace::setB(Mat &B) { B.copyTo(B_); } void StateSpace::setC(Mat &C) { C.copyTo(C_); } void StateSpace::setQ(Mat &Q) { Q.copyTo(Q_); } void StateSpace::setR(Mat &R) { R.copyTo(R_); } void StateSpace::setState(Vec &x) { x.copyTo(x_); } void StateSpace::setP(Mat &P) { P.copyTo(P_); } Vec StateSpace::getState() const { return x_; } Mat StateSpace::getP() const { return P_; } void StateSpace::update(Vec &u, Vec &y) { //K为卡尔曼增益矩阵 Mat K = P_ * C_.t() * (C_ * P_ * C_.t() + R_).inv(); //更新状态向量和状态协方差矩阵 x_ = A_ * x_ + B_ * u + K * (y - C_ * x_); P_ = (Mat::eye(n_, n_, CV_64FC1) - K * C_) * P_ * (Mat::eye(n_, n_, CV_64FC1) - K * C_).t() + K * R_ * K.t(); } int main() { //定义状态空间模型 StateSpace ss(1, 1); //设置模型参数 Mat A = Mat::ones(1, 1, CV_64FC1); Mat C = Mat::ones(1, 1, CV_64FC1); Mat Q = Mat::ones(1, 1, CV_64FC1) * 0.1; Mat R = Mat::ones(1, 1, CV_64FC1); Vec x0 = Vec::zeros(1); Mat P0 = Mat::ones(1, 1, CV_64FC1); ss.setA(A); ss.setC(C); ss.setQ(Q); ss.setR(R); ss.setState(x0); ss.setP(P0); //定义控制向量和观测向量 Vec u = Vec::zeros(1); Vec y = Vec::zeros(1); //预测未来5个时刻的状态 for (int i = 0; i < 5; i++) { //更新状态向量和状态协方差矩阵 ss.update(u, y); //输出当前状态向量 cout << "x" << i << ": " << ss.getState() << endl; } return 0; } ``` 运行结果如下: ``` x0: [0] x1: [0] x2: [0] x3: [0] x4: [0] ``` 可以看到,预测结果均为0,这是由于观测向量y一直为0所致。如果我们在每个时刻将y设置为一个随机值,就可以得到更有意义的预测结果了。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

breaksoftware

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值