看点:状态机编程实例-面向对象的状态设计模式
上篇文章:状态机编程实例-状态表法
使用状态表法,实现了炸弹拆除小游戏的状态机编程,这也是介绍的状态机编程的第二种方法。
本篇,继续介绍状态机编程的第三种方法:面向对象的设计模式。此方法从名字上看,用到了面向对象的思想,所以本篇的代码,需要以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++的友元特性。
2.2 类的具体实现2.2.1 状态基类与派生类友元包括友元函数与友元类,这里先介绍下本例使用到的友元类。
友元类的作用:如果把在A类(如本例中的上下文类Bomb3)中声明了友元类B(如本例中的SettingState和TimingState),那么A类的所有成员函数,可以被B类的所以成员函数访问。
友元使用前提:某个类需要实现某种功能,但是这个类自身,因为各种原因,无法自己实现,需要借助于“外力”才能实现。
本例中,SettingState和TimingState,需要借助上下文类Bomb3,实现状态转换等功能
体会友元类的用法: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++的继承特性,以及类指针,实现炸弹拆除小游戏中的状态机功能。
本篇,需要重点体会的点包括:
状态基类与派生类的关系虚函数与友元类的作用上下文类的使用指向对象的指针的使用审核编辑:汤梓红标签: