设计模式--状态模式(C语言实现)

2023-05-16

原创 亚索老哥 embed linux share 

模式动机

状态模式(状态机)是嵌入式开发中最重要、最核心的设计模式之一,毫不夸张的说,是否熟练掌握状态模式,很大程度上直接决定了嵌入式工程师的代码掌控能力。在嵌入式开发里面,几乎80%以上的程序都有状态模式(状态机)的影子。在一个思路清晰而且高效的程序中,必然有状态模式(状态机)身影浮现。但是很多嵌入式开发者只是掌握一些很基础的状态机编程,对状态机编程如果提高程序的可维护性和可拓展性并没有一个深刻的理解。

这里我通过一个简单易懂的MP3播放器案例,把自己独家总结的状态机六步法分享给大家,帮助大家在啃下状态机这块硬骨头。相信你深度掌握状态机编程以后,你优雅美观的代码会让同事朋友们眼前一亮,啧啧称赞。

生活中的状态模式(状态机)

几乎在所有的复杂项目里面,都充斥着各种事物状态的变化。这是因为我们身处的物理世界本来就是一个动态多变的环境,自然我们开发的程序也要根据事物不同时刻不同场景的状态,不断调整自身的行为属性。

比如电影《分裂》里面,詹姆斯·麦卡沃伊饰演的男主Kevin患有精神分裂,有着多重人格疾病,他被精神病医生Dr. Fletcher诊断出有23重人格,可以随时间或境遇切换,一会变成精明聪颖的律师,一会是懦弱的失败者总是要自杀,一个境遇触发又是愤怒的杀人暴徒,这人格切换速度,丧心病狂到令人发指。

想象一下,假如我们要在程序中实现这样一个角色,就必须要有一个良好的状态变化设计,才能保证主人公在快速切换状态的情况下,都能拥有与之匹配的精神状态和行为举止。

场景案例

场景:设计一个简单的MP3播放器,要求两个按键(播放/暂停、停止)分别控制MP3的播放/停止功能。

如下表所示:

按键功能
[play/pause]播放/暂停
[stop]停止

状态迁移图

在状态模式的设计开发中,我们通常借助状态迁移图来进行多个状态的分析。本案例中的MP3播放器,状态迁移图如下图所示:

虽然图示很简单,但是非常有用,因为各按键按下后,MP3播放器的状态变化一目了然,根据状态迁移图,我们就可以着手程序的编写了。

我们先来看一个状态模式(状态机)的入门级别的实现--简单状态机。其实就是通过大量的switch/case和if/else,在很多项目中经常可以看到类似的代码:

#include <stdio.h>

void stopPlayer();
void pausePlayer();
void resumePlayer();
void startPlayer();
//按键的动作类型
typedef enum {
    EV_STOP,
    EV_PLAY_PAUSE
}EventCode;

//MP3的状态
enum{
  ST_IDLE,
  ST_PLAY,
  ST_PAUSE
};

//MP3当前状态
char state;

//MP3状态初始化
void init()
{
  state = ST_IDLE;
}

//状态机处理MP3的过程变化
void onEvent(EventCode ec)
{
  switch (state)
  {
  case ST_IDLE:
        if(EV_PLAY_PAUSE == ec)
          startPlayer();
        break;
  case ST_PLAY:
        if(EV_STOP == ec)
          stopPlayer();
        else if(EV_PLAY_PAUSE == ec)
          pausePlayer();
        break;
  case ST_PAUSE:
        if(EV_STOP == ec)
          stopPlayer();
        else if(EV_PLAY_PAUSE == ec)
          resumePlayer();
        break;
  default:
        break;
  }
}

void stopPlayer()
{
  state = ST_IDLE;
  printf("停止播放音乐\n");
}

void pausePlayer()
{
  state = ST_PAUSE;
  printf("暂停播放音乐\n");
}

void resumePlayer()
{
  state = ST_PLAY;
  printf("恢复播放音乐\n");
}

void startPlayer()
{
  state = ST_PLAY;
  printf("开始播放音乐\n");
}
//主程序实现MP3的播放控制
void main()
{
  init();
  onEvent(EV_PLAY_PAUSE);//播放
  onEvent(EV_PLAY_PAUSE);//暂停
  onEvent(EV_PLAY_PAUSE);//继续播放
  onEvent(EV_STOP);      //停止
}

代码已经在c在线工具|菜鸟工具中运行验证,读者也可以自行验证。运行结果如下:

开始播放音乐
暂停播放音乐
恢复播放音乐
停止播放音乐

在上面的代码实现中,主要是在onEvent函数中,以MP3的当前状态作为判断条件进行相应的分支改动,简单地按照状态迁移图,实现了功能。

但是我们观察onEvent函数,不难发现其中有大量的swith...case这样的判断(if...else也是一样).对于MP3播放器这样简单的例子,这样的代码还是不难阅读和维护的。但是当状态和事件增加后,onEvent函数就会变得非常庞大,这是因为该函数的代码行数与状态和事件数量的乘积成正比,直接导致代码行数爆炸增长,代码会越发变得难以阅读和维护。

其次,程序的扩展性非常差,无论是我们新增一种状态,还是新增一种按键动作,onEvent函数都要大改特改,极难保障程序的稳定性。

解决方案

核心思路:我们可以利用C语言的多态特性来分解复杂的条件分支(关于c语言多态的实现,请查看c语言面向对象基础)。这样一来可以就避免大量的swith...case和 if...else等条件分支语句,提高程序的可维护性和可扩展性。

下面我将使用独家总结的六步法,帮助大家轻松掌握状态模式(状态机)的编程诀窍。

#include <stdio.h>

/***********************************************
1、定义状态接口,以MP3的状态接口为例,每种状态下都可能发生
两种按键动作。
************************************************/

typedef struct State{
  void (* stop)();
  void (* palyOrPause)();
}State;


/***********************************************
2、定义系统当前状态指针,保存系统的当前状态
************************************************/

State * pCurrentState;


/***********************************************
3、定义具体状态,根据状态迁移图来实现具体功能和状态切换。
************************************************/

void ignore();
void startPlay();
void stopPlay();
void pausePlay();
void resumePlay();

//空闲状态时,stop键操作无效,play/pause会开始播放音乐
State IDLE = {
  ignore,
  startPlay
};

//播放状态时,stop键会停止播放音乐,play/pause会暂停播放音乐
State PLAY = {
  stopPlay,
  pausePlay
};

//暂停状态时,stop键会停止播放音乐,play/pause会恢复播放音乐
State PAUSE = {
  stopPlay,
  resumePlay
};

void ignore()
{
  //空函数,不进行操作
}

void startPlay()
{
  //实现具体功能
  printf("开始播放音乐\n");
  //进入播放状态
  pCurrentState = &PLAY;
}
void stopPlay()
{
  //实现具体功能
  printf("停止播放音乐\n");
  //进入空闲状态
  pCurrentState = &IDLE;
}

void pausePlay()
{
  //实现具体功能
  printf("暂停播放音乐\n");
  //进入暂停状态
  pCurrentState = &PAUSE;
}

void resumePlay()
{
  //实现具体功能
  printf("恢复播放音乐\n");
  //进入播放状态
  pCurrentState = &PLAY;
}


/***********************************************
4、定义主程序上下文操作接口,主程序只关心当前状态,不关心状态之间
是怎么变化的。
************************************************/

void onStop();
void onPlayOrPause();

State context = {
  onStop,
  onPlayOrPause
};

void onStop(State *pThis)
{
  pCurrentState->stop(pThis);
}

void onPlayOrPause(State *pThis)
{
  pCurrentState->palyOrPause(pThis);
}


/***********************************************
5、初始化系统当前状态指针,其实就是指定系统的起始状态
************************************************/

void init()
{
  pCurrentState = &IDLE;
}

/***********************************************
6、主程序通过上下文操作接口来控制系统当前状态的变化
************************************************/
void main()
{
  init();
  context.palyOrPause();//播放
  context.palyOrPause();//暂停
  context.palyOrPause();//播放
  context.stop();//停止
}

代码已经在c在线工具|菜鸟工具中运行验证,读者也可以自行验证。运行结果如下:

开始播放音乐
暂停播放音乐
恢复播放音乐
停止播放音乐

对比前后两份代码,六步法实现的状态机比简单状态机明显有以下几方面的优点:

  • 代码结构要更加清晰,避免了过多的switch...case或者if...else语句   的使用。

  • 很好地体现了开闭原则和单一职责原则,每个状态都是一个子结构体,你要增加状态就要增加子结构体,你要修改状态,你只修改一个子结构体就可以了。

  • 封装性非常好,状态变换放置到子结构体的内部来实现,外部的调用不用知道子结构体的内部如何实现状态和行为的变换。

最后跟大家总计一下状态机六步法:

(1)、定义状态接口。
(2)、定义系统当前状态指针。
(3)、定义具体状态,根据状态迁移图来实现具体功能和状态切换。
(4)、定义主程序上下文操作接口。
(5)、初始化系统当前状态指针。
(6)、主程序通过上下文操作接口来控制系统当前状态的变化。

一般来说,熟练使用状态机六步法的嵌入式开发者,大都是两年软件开发经验以上的老鸟了。所以,如果你还是个嵌入式新手,请在实际开发中多多运用它,以后你的代码才能越来越优雅美观。而且掌握状态机编程对理解其他更复杂的设计模式也是大有裨益的。

 

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

设计模式--状态模式(C语言实现) 的相关文章

随机推荐

  • E: 无法定位软件包 ncurses 问题解决

    在编译Linux内核 xff08 make menuconfig xff09 之前 xff0c 我们需要安装ncurses xff0c 但是安装过程中提示 xff1a E 无法定位软件包 ncurses 解决办法 xff1a 系统可能识别不
  • 如何写H5/web前端工程简历中的项目经验

    有不少前端工程师 xff0c 在写简历时就发愁 简历中的项目怎么写 xff0c 怎么描述 觉得自己虽然工作了好几年 xff0c 做过许多项目 xff0c 但是觉得都没做什么高大上的事情 xff0c 自然就觉得没啥可写的 或者觉得做的事情都一
  • 问题解决:error: #error “Please port gnulib fseterr.c to your platform! Look at the definitions of ferror

    一 遇到的问题 xff1a 编译Hi518EV300 LiteOS 时遇到如下问题 xff1a lib span class token operator span fseterr span class token punctuation
  • 解决问题:VScode 打印中文时终端输出乱码

    问题 xff1a 如下图所示 xff0c 终端打印中文时输出乱码 原因 xff1a VScode终端是在调用 cmd exe cmd 的默认编码格式是 GBK2312 xff0c 而VScode的默认编码格式是UTF 8 xff0c 把二者
  • C语言中voliate关键字的作用

    一个变量用voliate关键字修饰 xff0c 是要告诉编译器 xff0c 这个变量的值随时可能发生变化 xff0c 所以编译器每次都必须从变量对应的内存地址中获取该变量的值 voliate的常用方式 xff1a 当一个变量表示某寄存器的值
  • Makefile 编写教程(由简至难)

    目录 一 测试代码二 c 文件与Makefile同级三 c文件与Makefile不同级四 链接静态库 一 测试代码 本文以将main c add c common h三个文件编译成一个可执行文件为例 xff0c 来讲解Makefile的编写
  • Ubuntu解决 The following signatures couldn‘t be verified because the public key is not available:

    Ubuntu在source list中添加源后 xff0c 提示 The following signatures couldn t be verified because the public key is not available N
  • Ubuntu搭建gitlab-Ci教程

    本文参考gitlab官方教程搭建CI GitLab Runner是一款开源工具 xff0c 用于在GitLab上运行CI CD xff08 持续集成 持续交付 xff09 作业 它可以在多个操作系统上安装和使用 xff0c 并且可以在容器中
  • 常用命令汇总

    tar gz 压缩包 解压 xff1a tar zxvf FileName tar gz 压缩 xff1a tar zcvf FileName tar gz DirName 删除远程分支 span class token function
  • 问题解决:import serial提示 ImportError: No module named serial

    问题 xff1a 在Ubuntu系统中运行python脚本 xff0c 出现如下提示说明pythoh的serial模块没有安装 ImportError No module named serial 解决 网上大部分的解决方法都是安装pyse
  • An Introduction to Deep Learning for the PhysicalLayer

    An Introduction to Deep Learning for the PhysicalLayer I INTRODUCTION 这段主要讲了文章的主要工作 将发射机 通道和接收器作为一个自动编码器 xff0c 对于给定的损失函数
  • 机器学习调制解调器:ML将如何改变我们设计下一代通信系统的方式

    所以 xff0c 我听到你们都在问 xff0c 我们都失业了吗 通信工程师是机器崛起的又一个受害者吗 幸运的是 xff0c 本文中的答案是否定的 但是Nathan Ben和Tim指出了一种新的方式来指定和设计通信系统 xff0c 这可能会永
  • git clone指定分支

    技术背景 Git是代码版本最常用的管理工具 xff0c 此前也写过一篇介绍Git的基本使用的博客 xff0c 而本文介绍一个可能在特定场景下能够用到的功能 直接拉取指定分支的内容 Git Clone 首先看一下如果我们按照常规的操作去拉取一
  • PX4最新版ubuntu编译环境搭建

    PX4最新版ubuntu编译环境搭建 本博客的撰写主要也是博主的自己血泪之路 xff0c 参考了各种博客 xff0c 然而对于我并没有什么卵用 xff0c 简直是成功的都一样 xff0c 不成功的各有各的不同 xff0c 最后在快要放弃而转
  • RTOS滴答Tick设置多少才合适

    本文转载 xff0c 留作笔记 xff0c 如有侵权 xff0c 请联系删除 xff0c 原文链接地址 xff1a 嵌入式开发 RTOS滴答Tick设置多少才合适 xff1f qq com https mp weixin qq com s
  • Darknet-Deep_sort_pytorch 无人机跟踪识别记录

    创建数据集 使用labelme 构造voc数据集格式 转换txt为xml 开始训练 span class token function sudo span span class token function nohup span darkn
  • Docker安装Ubuntu

    local footstep 64 ubuntu ifconfig docker0 flags 61 4163 lt UP BROADCAST RUNNING MULTICAST gt mtu 1500 inet 172 17 0 1 ne
  • MSCKF学习记录

    MSCKF相关资料 1 github参考实现 xff1a daniilidis group msckf mono https github com daniilidis group msckf mono 2 CSND参考博客 xff1a m
  • ARM linux 串口接收

    C语言read函数的使用以及串口初始化的调用 废话不多说 xff0c 直接上代码 xff0c 有疑问可以看我之前的文章 https blog csdn net m0 38053897 article details 108816643 ht
  • 设计模式--状态模式(C语言实现)

    原创 亚索老哥 embed linux share 模式动机 状态模式 状态机 是嵌入式开发中最重要 最核心的设计模式之一 xff0c 毫不夸张的说 xff0c 是否熟练掌握状态模式 xff0c 很大程度上直接决定了嵌入式工程师的代码掌控能