0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

状态机编程实例-面向对象的状态设计模式

码农爱学习 来源:码农爱学习 作者:码农爱学习 2023-06-28 09:04 次阅读

上篇文章:状态机编程实例-状态表法

使用状态表法,实现了炸弹拆除小游戏的状态机编程,这也是介绍的状态机编程的第二种方法。

本篇,继续介绍状态机编程的第三种方法:面向对象的设计模式。此方法从名字上看,用到了面向对象的思想,所以本篇的代码,需要以C++为基础,利用C++中“类”的特性,实现状态机中状态的管理。

1 面向对象的状态设计模式

面向对象的状态设计模式,其核心思想在于:它是通过不同的类来表示不同的状态,当状态机从一个状态转换到另一个状态时,它表现为在运行时改变自己的类。

回顾第一篇时绘制的炸弹拆除小游戏的状态图,有2个状态和4个事件:

使用面向对象的状态设计模式,此例子中的****两个工作状态,就要设计为两个类,如下图中的设置状态(SettingState)和倒计时状态(TimingState)。

先简单说明一下下面这个图,此图属于UML类图,相关介绍可参考: UML简介与类图详解

Bomb3与BombState是组合关系,BombState是一个抽象类,SettingState与TimingState继承自BombState,属于继承关系

可以注意到,此模式引入了一个炸弹状态的****抽象基类BombState,用于派生具体的工作状态类。

该抽象类为炸弹的两个工作状态声明了一些公共的接口:onUP、onDOWN、onARM和onTICk,这些接口对应于此例子中的四个事件

两个工作状态类:SettingState类和TimingState类,通过定义自己的onUP等操作,实现各自状态类需要处理的功能。

这种设计模式下:

  • 如果需要增加新的事件,则需要给抽象类BombState增加新的操作
  • 如果需要增加新的状态,则需要给抽象类BombState增加新的子类

此模式还设计了一个上下文类Bomb3,它通过一个抽象类BombState的指针来实现炸弹状态的维护。

什么是上下文?

编程中提到的上下文(context),可以理解为环境或语境,每一段程序都有很多的外部变量,一旦写的一段程序中有了外部变量,这段程序就是不完整的,不能独立运行,要想让他运行,就必须把所有的外部变量的值一个一个的全部传进去,这些值的集合就叫作上下文。

本例中,BombState的运行,就需要一个上下文类作为其参数,这个参数就是Bomb3类。

此外,它还包含需要用到的****扩展状态变量

  • timeout(超时时间)
  • code(用户输入的拆除密码)
  • defuse(默认的拆除密码)

并通过提供对BombState一样的接口,即每派生一个事件对应一个操作。

在上下文类Bomb3中的事件处理,是通过state_指针实现的,它代表了对当前状态对象的全部特定请求,状态的改变对应于当前工作状态类对象的改变,通过上下文操作tran()实现。

2 实现

介绍了面向对象的状态设计模式后,下面来看下如何使用C++语言进行对应的代码实现。

2.1 类的结构

首先来看下要实现的几个类的结构定义。

2.1.1 状态基类与派生类

下面是炸弹状态基类(BombState)的结构,以及派生的两个具体状态类(SettingState和TimingState)的结构。

class Bomb3; //事先声明炸弹业务类//炸弹状态基类
class BombState
{
  public:
    virtual void onUP(Bomb3 *) const {}
    virtual void onDOWN(Bomb3 *) const {}
    virtual void onARM(Bomb3 *) const {}
    virtual void onTICK(Bomb3 *, uint8_t) const {}
};
​
//设置状态-类,继承于炸弹状态基类
class SettingState : public BombState
{
  public:
    virtual void onUP(Bomb3 *context) const;
    virtual void onDOWN(Bomb3 *context) const;
    virtual void onARM(Bomb3 *context) const;
};
​
//倒计时状态-类,继承于炸弹状态基类
class TimingState : public BombState
{
  public:
    virtual void onUP(Bomb3 *context) const;
    virtual void onDOWN(Bomb3 *context) const;
    virtual void onARM(Bomb3 *context) const;
    virtual void onTICK(Bomb3 *context, uint8_t fine_time) const;
};

注意这里用到了C++虚函数的特性。

虚函数,是指被virtual关键字修饰的成员函数。

虚函数的作用:

  • 实现动态联编,在函数运行阶段动态的选择合适的成员函数
  • 实现多态性(Polymorphism),多态性是将接口与实现进行分离;用形象的语言来解释就是实现以共同的方法,但因个体差异,而采用不同的策略。

虚函数主要通过V-Table虚函数表来实现,该表主要包含一个类的虚函数的地址表,可解决继承、覆盖的问题。当我们使用一个父类的指针去操作一个子类时,虚函数表就像一个地图一样,可指明实际所应该调用的函数。

此外,对事件的处理,用到了指向类对象的指针(Bomb3 *context

指针也就是内存地址,指针变量是用来存放内存地址的变量,不同类型的指针变量所占用的存储单元长度是相同的,而存放数据的变量因数据的类型不同,所占用的存储空间长度也不同。

有了指针以后,不仅可以对数据本身,也可以对存储数据的变量地址进行操作。

创建对像时,编译系统会为每一个对像分配一定的存储空间,以存放其成员,对象空间的起始地址就是对象的指针。可以定义一个指针变量,用来存和对象的指针。

2.1.2 炸弹业务类

炸弹业务类,也就是上面提到的上下文类。

class Bomb3
{
  public:
    Bomb3(uint8_t defuse) : m_defuse(defuse) {}
​
    void init(); //状态机初始化接口//处理各种事件
    void onUP();
    void onDOWN();
    void onARM();
    void onTICK(uint8_t fine_time);
​
  private:
    //进行状态转换
    void tran(BombState const *target);
​
  private:
    BombState const *m_pState; //[状态变量]
    uint8_t m_timeout; // 爆炸前的秒数
    uint8_t m_code;    // 当前输入的解除炸弹的密码
    uint8_t m_defuse;  // 解除炸弹的拆除密码
    uint8_t m_errcnt;  // 当前拆除失败的次数private:
    SettingState const m_settingState; //[设置状态]
    TimingState const m_timingState; //[倒计时状态]friend class SettingState;
    friend class TimingState;
};

注意这里又用到了C++的友元特性。

友元包括友元函数与友元类,这里先介绍下本例使用到的友元类

友元类的作用:如果把在A类(如本例中的上下文类Bomb3)中声明了友元类B(如本例中的SettingState和TimingState),那么A类的所有成员函数,可以被B类的所以成员函数访问。

友元使用前提:某个类需要实现某种功能,但是这个类自身,因为各种原因,无法自己实现,需要借助于“外力”才能实现。

本例中,SettingState和TimingState,需要借助上下文类Bomb3,实现状态转换等功能

2.2 类的具体实现

2.2.1 状态基类与派生类

体会友元类的用法:Bomb3中声明了SettingState是友元,SettingState则可以访问Bomb3的成员变量(如m_timeout变量)和成员函数(如tran函数)。

体会上下文类Bomb3的作用:设置状态SettingState和倒计时状态TimingState,都是操作Bomb3这个上下文类,实现对应状态下的业务功能。

//---------------设置状态-类,具体实现---------------
void SettingState::onUP(Bomb3 *context) const
{
  if (context- >m_timeout < 60)
  {
    ++context- >m_timeout;
    bsp_display_set_time(context- >m_timeout);
  }
}
void SettingState::onDOWN(Bomb3 *context) const
{
  if (context- >m_timeout > 1)
  {
    --context- >m_timeout;
    bsp_display_set_time(context- >m_timeout);
  }
}
void SettingState::onARM(Bomb3 *context) const
{
  context- >m_code = 0;
  context- >tran(&context- >m_timingState); //[转换到倒计时状态]
}
​
//---------------倒计时状态-类,具体实现---------------
void TimingState::onUP(Bomb3 *context) const
{
  context- >m_code < <= 1;
  context- >m_code |= 1;
  bsp_display_user_code(context- >m_code);
}
void TimingState::onDOWN(Bomb3 *context) const
{
  context- >m_code < <= 1;
  bsp_display_user_code(context- >m_code);
}
void TimingState::onARM(Bomb3 *context) const
{
  if (context- >m_code == context- >m_defuse)
  {
    context- >tran(&context- >m_settingState); //[转换到设置状态]
    bsp_display_user_success(); //炸弹拆除成功
    context- >init();
  }
  else
  {
    context- >m_code = 0;
    bsp_display_user_code(context- >m_code);
    bsp_display_user_err(++context- >m_errcnt);
  }
}
void TimingState::onTICK(Bomb3 *context, uint8_t fine_time) const
{
  if (fine_time == 0)
  {
    --context- >m_timeout;
    bsp_display_remain_time(context- >m_timeout);
    if (context- >m_timeout == 0)
    {
      bsp_display_bomb(); //显示爆炸效果
      context- >init();
    }
  }
}

2.2.2 炸弹业务类

炸弹业务类,提供通用的事件处理接口:onUP、onDOWN、onARM和onTICK,其内部具体如果处理,是由m_pState指向的具体状态类决定的,状态指针m_pState的改变,是通过tran函数实现的,tran在初始转换和具体的状态类的成员函数中被调用。

//初始化
void Bomb3::init()
{
  m_timeout = INIT_TIMEOUT;
  m_errcnt  = 0;
  tran(&m_settingState); //[初始转换]
}
//处理各种事件
void Bomb3::onUP()
{
  m_pState- >onUP(this);
}
void Bomb3::onDOWN()
{
  m_pState- >onDOWN(this);
}
void Bomb3::onARM()
{
  m_pState- >onARM(this);
}
void Bomb3::onTICK(uint8_t fine_time)
{
  m_pState- >onTICK(this, fine_time);
}
//进行状态转换
void Bomb3::tran(BombState const *target) 
{
  m_pState = target;
}

2.3 主函数

使用面向对象的状态设计模式,炸弹拆除小游戏的主函数会比较简洁:

  • 首先实例化一个Bomb3上下文类的实例bomb
  • 然后进行bomb的初始化(状态转换)
  • 最后在状态机循环中,根据不同的按键或TICK事件,调用bomb对应的事件处理接口

体会,本例的事件处理,调用的是通用的bomb事件处理接口,其内部会根据当前的具体状态,调用对应状态类的事件处理函数。

static Bomb3 bomb(0x0D); // 构造, 密码1101void setup(void)
{
  //省略...
  bomb.init(); // 初始转化
}
​
void loop(void)
{
  static int fine_time = 0;
  delay(100);
​
  if (++fine_time == 10)
  {
    fine_time = 0;
  }
​
  char tmp_buffer[256];
  sprintf(tmp_buffer, "T(%1d)%c", fine_time, (fine_time == 0) ? '\\n' : ' ');
  Serial.print(tmp_buffer);
​
  bomb.onTICK(fine_time); //处理Tick事件
​
  BombSignals userSignal = bsp_key_check_signal();
  if (userSignal != SIG_MAX)
  {
    switch (userSignal)
    {
      case UP_SIG: //UP键事件
      {
        Serial.print("\\nUP  : ");
        bomb.onUP();
        break;
      }
      case DOWN_SIG: //DOWN键事件
      {
        Serial.print("\\nDOWN: ");
        bomb.onDOWN();
        break;
      }
      case ARM_SIG: //ARM键事件
      {
        Serial.print("\\nARM : ");
        bomb.onARM();
        break;
      }
      default:break;
    }
  }
}

3 总结

本编介绍了状态机编程的第3种方法——面向对象的状态设计模式,通过C++的继承特性,以及类指针,实现炸弹拆除小游戏中的状态机功能。

本篇,需要重点体会的点包括:

  • 状态基类与派生类的关系
  • 虚函数与友元类的作用
  • 上下文类的使用
  • 指向对象的指针的使用
    审核编辑:汤梓红
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • 嵌入式
    +关注

    关注

    4983

    文章

    18286

    浏览量

    288506
  • 编程
    +关注

    关注

    88

    文章

    3441

    浏览量

    92406
  • 状态机
    +关注

    关注

    2

    文章

    486

    浏览量

    27182
收藏 人收藏

    评论

    相关推荐

    STM32状态机编程实例——全自动洗衣机(下)

    本篇在上篇全自动洗衣机的状态机编程实例的基础上,增加了OLED来更新直观的展示洗衣机的工作状态,并通过3种测试场景来展示洗衣机工作状态机的执
    的头像 发表于 09-07 08:47 2734次阅读
    STM32<b class='flag-5'>状态机</b><b class='flag-5'>编程</b><b class='flag-5'>实例</b>——全自动洗衣机(下)

    状态机编程实例-状态表法

    上篇文章,使用嵌套switch-case法的状态机编程,实现了一个炸弹拆除小游戏。本篇,继续介绍状态机编程的第二种方法:状态表法,来实现炸弹
    的头像 发表于 06-20 09:05 1261次阅读
    <b class='flag-5'>状态机</b><b class='flag-5'>编程</b><b class='flag-5'>实例</b>-<b class='flag-5'>状态</b>表法

    状态机编程实例-嵌套switch-case法

    嵌入式软件开发中,状态机编程是一个比较实用的代码实现方式,特别适用于事件驱动的系统。本篇,以一个炸弹拆除的小游戏为例,介绍状态机编程的思路。
    的头像 发表于 06-15 09:01 1173次阅读
    <b class='flag-5'>状态机</b><b class='flag-5'>编程</b><b class='flag-5'>实例</b>-嵌套switch-case法

    Verilog状态机+设计实例

    在verilog中状态机的一种很常用的逻辑结构,学习和理解状态机的运行规律能够帮助我们更好地书写代码,同时作为一种思想方法,在别的代码设计中也会有所帮助。 一、简介 在使用过程中我们常说
    的头像 发表于 02-12 19:07 2009次阅读
    Verilog<b class='flag-5'>状态机</b>+设计<b class='flag-5'>实例</b>

    状态机编程

    状态机编程基于状态机的按键输入软件接口设计一般的教课书中给出的按键输入软件接口程序通常非常简单,在程序中一旦检测到按键输入口为低电平时(图9-2),便采用(调用)软件延时程序延时10ms。然后再
    发表于 07-10 18:00

    raw os 之状态机编程

    状态机编程的历史很可能久于传统的操作系统, 传统的一个大while 循环模式普遍用到了状态机模式编程
    发表于 02-27 14:35

    状态机

    控制状态机控制状态机的初始化和状态转换的最佳方法是使用枚丽型输入控件。一般使用自定义类型的枚丽变量。使用子定义类型的枚丽变量可以是控件和实例乊间存在关联,使得添加或删除
    发表于 02-13 12:39

    什么是状态机状态机是如何编程的?

    什么是状态机状态机是如何编程的?
    发表于 10-20 07:43

    什么是状态机

    目录1 前言2 状态机2.1 什么是状态机2.2 状态机的概念2.3 使用状态机写键盘的思路3 代码实例3.1 使用软件3.2 protue
    发表于 01-24 06:23

    状态机实例(VHDL源代码)

    状态机实例(VHDL源代码):
    发表于 05-27 10:27 59次下载
    <b class='flag-5'>状态机</b><b class='flag-5'>实例</b>(VHDL源代码)

    状态机概述 如何理解状态机

    本篇文章包括状态机的基本概述以及通过简单的实例理解状态机
    的头像 发表于 01-02 18:03 1w次阅读
    <b class='flag-5'>状态机</b>概述  如何理解<b class='flag-5'>状态机</b>

    什么是状态机状态机5要素

    玩单片机还可以,各个外设也都会驱动,但是如果让你完整的写一套代码时,却无逻辑与框架可言。这说明编程还处于比较低的水平,你需要学会一种好的编程框架或者一种编程思想!比如模块化编程
    的头像 发表于 07-27 11:23 1.9w次阅读
    什么是<b class='flag-5'>状态机</b>?<b class='flag-5'>状态机</b>5要素

    状态模式(状态机)

    以前写状态机,比较常用的方式是用 if-else 或 switch-case,高级的一点是函数指针列表。最近,看了一文章《c语言设计模式状态模式(
    发表于 12-16 16:53 7次下载
    <b class='flag-5'>状态</b><b class='flag-5'>模式</b>(<b class='flag-5'>状态机</b>)

    c语言设计模式--状态模式(状态机)

    状态模式(状态机)是嵌入式开发中最重要、最核心的设计模式之一,毫不夸张的说,是否熟练掌握状态模式
    的头像 发表于 06-14 15:28 633次阅读
    c语言设计<b class='flag-5'>模式</b>--<b class='flag-5'>状态</b><b class='flag-5'>模式</b>(<b class='flag-5'>状态机</b>)

    什么是有限状态机?有限状态机的四要素介绍

    如果一个对象(系统或机器),由若干个状态构成,在某种条件下触发这些状态,会发生状态相互转移的事件,那么此对象称之为
    的头像 发表于 09-17 16:42 1720次阅读