设计模式|状态模式及其在状态机的中的使用

状态模式

使用场景

在一个类的内部会存在多种状态的变化,状态的变化会引起对象的行为、动作或属性发生变化,在类的外部就如类的自身发生了变化。此时可以使用状态模式对类在不同状态下的变化进行管理,增加代码的可读性。

典型案例

对于一个类,其一天内的工作内容会随着时间的变化而变化,此时若将工作视为该类的一个成员函数,该成员函数的动作会随着时间改变而改变。为增加代码的复用性,可将状态定义为一个基类,在定义不同时间段的子类时继承该基类,并在子类中针对不同的状态对基类中的虚函数进行重载。请添加图片描述

class Sanji;//仅声明,供定义时调用
class AbstractState
{
public:
    virtual void working(Sanji* sanji) = 0;
    virtual ~AbstractState() {}
};

其中,working函数为纯虚函数,可在子类中进行重载,以实现对不同状态下的工作进行差异化定义。
再定义不同时间段的子类,如

// 上午状态
class ForenoonState : public AbstractState
{
public:
    void working(Sanji* sanji) override;
};

// 中午状态
class NoonState : public AbstractState
{
public:
    void working(Sanji* sanji) override;
};

// 下午状态
class AfternoonState : public AbstractState
{
public:
    void working(Sanji* sanji) override;
};

// 晚上状态
class EveningState : public AbstractState
{
public:
    void working(Sanji* sanji) override;
};

定义成员函数

#include"State.h"
#include"sanji.h"
#include<iostream>


using namespace std;

void ForenoonState::working(Sanji* sanji)
{
    int time = sanji->getClock();
    if (time < 8)
    {
        cout << "当前时间<" << time << ">点, 准备早餐, 布鲁克得多喝点牛奶..." << endl;
    }
    else if (time > 8 && time < 11)
    {
        cout << "当前时间<" << time << ">点, 去船头钓鱼, 储备食材..." << endl;
    }
    else
    {
        sanji->setState(new NoonState);
        sanji->working();
    }
}

void NoonState::working(Sanji* sanji)
{
    int time = sanji->getClock();
    if (time < 13)
    {
        cout << "当前时间<" << time << ">点, 去厨房做午饭, 给路飞多做点肉..." << endl;
    }
    else
    {
        sanji->setState(new AfternoonState);
        sanji->working();
    }
}

void AfternoonState::working(Sanji* sanji)
{
    int time = sanji->getClock();
    if (time < 15)
    {
        cout << "当前时间<" << time << ">点, 准备下午茶, 给罗宾和娜美制作爱心甜点..." << endl;
    }
    else if (time > 15 && time < 18)
    {
        cout << "当前时间<" << time << ">点, 和乔巴去船尾钓鱼, 储备食材..." << endl;
    }
    else
    {
        sanji->setState(new EveningState);
        sanji->working();
    }
}

void EveningState::working(Sanji* sanji)
{
    int time = sanji->getClock();
    if (time < 19)
    {
        cout << "当前时间<" << time << ">点, 去厨房做晚饭, 让索隆多喝点汤..." << endl;
    }
    else
    {
        cout << "当前时间<" << time << ">点, 今天过得很高兴, 累了睡觉了..." << endl;
    }
}

定义sanji类实现获取时间、修改状态以及调用工作接口

#pragma once
#include"sanji.h"
#include"State.h"

class Sanji
{
public:
    Sanji()
    {
        m_state = new ForenoonState;
    }
    void working()
    {
        m_state->working(this);
    }
    void setState(AbstractState* state)
    {
        if (m_state != nullptr)
        {
            delete m_state;
        }
        m_state = state;
    }
    void setClock(int time)
    {
        m_clock = time;
    }
    int getClock()
    {
        return m_clock;
    }
    ~Sanji()
    {
        delete m_state;
    }
private:
    int m_clock = 0;    // 时钟
    AbstractState* m_state = nullptr;
};

主函数中模拟时间变化,保存时间点并通过for循环对时间点进行遍历,做出对应工作

#include<iostream>
#include<vector>
#include"sanji.h"
#include"State.h"

using namespace std;
int main()
{
    Sanji* sanji = new Sanji;
    // 时间点
    vector<int> data{ 7, 10, 12, 14, 16, 18, 22 };
    for (const auto& item : data)
    {
        sanji->setClock(item);
        sanji->working();
    }
    delete sanji;

    return 0;
}

运行结果
在这里插入图片描述
在不同的状态中,通过当前时间判断是否需要进行状态变化,并重新对sanji类中的状态进行更新。

状态机

状态机可以理解为是一种描述事物不同状态的数学模型,用于表示事物在什么情况下状态会发生改变并在该状态下会执行什么动作。状态模式是一种设计代码的方法,我们可以使用状态模式的思想来对状态机这一数学模型进行代码实现。
如上述例子使用状态机绘制出状态变化的过程即为一条状态随时间变化的单向状态变化图。

  • 6
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
状态模式是GoF23个模式最常用的之一,这篇小文不打算涉及方方面面的内容,只想在状态模式的高效运用方面谈一下自己的心得体会。   状态模式是用来设计状态机的,因此下面的叙述将它们等同理解。有关状态机设计方面的书籍,我这里隆重推荐一本:《Practical Statecharts in C/C++ Quantum Programming for Embedded Systems》,文名叫做《嵌入式系统的微模块化程序设计-实用状态图C/C++实现》,北航出版的,作者是Miro Samek博士,长期从事嵌入式实时系统的开发,具有丰富的经验。如果你想对状态机领域进行比较深入的研究,这本书绝对不容错过。   让我们先来看看比较“古老”的状态机实现,假设你还是用C语言。一般而言,我们用得到状态机系统都可以称为事件(消息)驱动系统,系统往往处于某个状态,等待外部的激励。这些激励可以是外部的事件、定时器超时等等,系统收到这些事件后,进行相应的处理,然后跃迁到新的状态状态也可能不变)继续等待下一个激励的到来,最后直到相应的事务处理完毕为止。   典型的状态机实现需要考虑几个要素:状态、消息(及其内容)、消息处理函数以及系统上下文等。系统处于某个状态,收到某个消息后,解析出消息内容,然后调用相应的消息处理函数进行处理,而消息处理函数往往会用到状态机的上下文数据,消息处理完毕系统会跃迁到新的状态。   典型代码大致如下:   switch (state)   {    case STATE1:    switch (msg)    {    case MSG1:    HandleMsg1(msgpara,context);    nextstate(STATE2);    break;    case MSG2:    HandleMsg2(msgpara,context);    nextstate(STATE3);    break;    /*......*/    }    case STATE2:    switch (msg)    {    case MSG3:    HandleMsg3(msgpara,context);    nextstate(STATE3);    break;    /*......*/    }    /*......*/   }   可以看到这就是所谓的平面状态机,特点就是先枚举状态,然后再枚举消息,如果找不到的话,就将消息丢弃。   为了使状态机更高效的运行,这里有几个小技巧,稍为总结一下。   (1)把接收概率大的消息放在前面   把同一个状态下最有可能收到的消息放在前面。一个状态下可能要处理很多消息,这视乎你状态划分的粒度大小。每个消息收到的机会并不是均等的,有些消息系统收到的概率很大,有些很小,因此把接收概率大的消息放在前面,这样可以减少case消息时的比较次数,相应的执行效率就提高了。对于一个状态机的运行而言,这样的节省当然微乎其微,但假如你的系统同时运行成千上万个这种状态机时,那么就有必要考虑一下这种优化了。   (2)查表法   第(1)种方法再怎么优化,也需要枚举状态和消息,假如能把这方面的开销变成零,那么效率自然可以进一步提升。我们可以想象把消息处理函数指针放在一个二维数组(表),其一维代表状态,另外一维代表消息序号,那么通过p[state][msg]就可以定位到当前状态下当前消息的处理函数。对一些简单的应用,甚至可以把新状态也存放在这张二维表,这样的好处是用户不需要显示调用状态跃迁函数。当然对于一些状态有不同执行路径的情况,状态的跃迁可能就要放在消息处理函数之。   (3)消息先分段再查表   一般而言,一个状态机状态数目不会很多,当然接收的消息数目也是有限的。但一般来说,消息是不连续的,这样应用查表法可能内存的开销就比较大,尤其是消息序号比较稀疏的时候,内存更加浪费。   在一般的嵌入式软件开发,我发现往往可以将消息进行归类分段,比方说一个接口的消息定义成一段。这样虽然消息不连续,但通过分段后可以将消息放在一个较紧凑的内存空间,在这个空间里再运用查表法,就有可能达到效率和空间开销的平衡。注意,我是说有可能,并不是一定,这取决于具体情况。系统收到消息后,先判断消息处于哪个分段,然后调用p[state][msg - offset]来进行处理

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值