C 状态模式

嵌入式开发中最重要的当属状态模式(State),因为几乎所有的嵌入式开发都需要编写代码来响应硬件的各种状态。因此,代码中通常要使用各种分支结构来处理硬件的各种状态。

通过简单的CD播放器为例,逐步学习一下:

一、代码如下:

 

//
//  cdplayer.c
//  TestXianDaiC
//
//  Created by kangxg on 2018/2/3.
//  Copyright © 2018年 zy. All rights reserved.
//

#include "cdplayer.h"

typedef enum{
    EV_STOP,
    EV_PLAT_PAUSE,
} EventCode;
void onEvent(EventCode ec);

bool playflag;
bool pauseflag;

void  initialize(){
    playflag   = false;
    pauseflag = false;
    
    onEvent(EV_PLAT_PAUSE);
    onEvent(EV_PLAT_PAUSE);
    onEvent(EV_PLAT_PAUSE);
    onEvent(EV_STOP);
}



void onEvent(EventCode ec)
{
    switch (ec) {
        case EV_STOP:
        {
            if (playflag == true || pauseflag == true )
            {
                stopPlayer();
            }
        }
            break;
        case EV_PLAT_PAUSE:
        {
            if (playflag== true)
            {
                pausePlayer();
            }
            else if (pauseflag == true)
            {
                resumePlayer();
            }
            else
            {
                startPlayer();
            }
        }
            break;
        default:
            break;
    }
}
void stopPlayer()
{
    pauseflag = false;
    playflag  = false;
    printf("停止播放\n");
}

void pausePlayer()
{
    pauseflag = true;
    playflag  = false;
    printf("暂停播放\n");
}

void resumePlayer()
{
    pauseflag = false;
    playflag  = true;
    printf("恢复播放\n");
}


void startPlayer()
{
    pauseflag = false;
    playflag  = true;
    printf("开始播放\n");
}

   

   

调用方法运行:



initialize();
输出:
开始播放
暂停播放
恢复播放
停止播放

 

虽然上面代码满足了播放器的这些功能,但是阅读起来有些困难。代码中使用了playflag和 pauseflag 来保存播放器状态,但是一旦开始使用标志位,当程序扩展时就需要不断地增加标志位,这将导致程序越来越难以理解。而且,给标志位设值的规则也很难理解。如果另一个人在修改或是扩展代码时查看了pausePlayer函数,以暂停状态的playflag 的值为false为前提编写代码,就会导致程序在某些特定情况下无法正常工作。

再次进行更改:


//
//  cdplayer.c
//  TestXianDaiC
//
//  Created by kangxg on 2018/2/3.
//  Copyright © 2018年 zy. All rights reserved.
//

#include "cdplayer.h"

typedef enum{
    EV_STOP,
    EV_PLAY_PAUSE,
} EventCode;

typedef enum{
    ST_IDLE,
    ST_PLAY,
    ST_PAUSE,
} State;
void onEvent(EventCode ec);

bool playflag;
bool pauseflag;
State state;
void  initialize(){
    
    state = ST_IDLE;
    onEvent(EV_PLAY_PAUSE);
    onEvent(EV_PLAY_PAUSE);
    onEvent(EV_PLAY_PAUSE);
    onEvent(EV_STOP);
}



void onEvent(EventCode ec)
{
    switch (state) {
        case ST_IDLE:
        {
            if (ec == EV_PLAY_PAUSE )
            {
                startPlayer();
            }
        }
            break;
        case ST_PLAY:
        {
            switch (ec) {
                case EV_STOP:
                {
                    stopPlayer();
                }
                    break;
                case EV_PLAY_PAUSE:
                {
                    pausePlayer();
                }
                    break;
                default:
                    break;
            }
        }
            break;
        case ST_PAUSE:
        {
            switch (ec) {
                case EV_STOP:
                    stopPlayer();
                    break;
                case EV_PLAY_PAUSE:
                    resumePlayer();
                default:
                    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");
}


将状态作为变量可以很好地避免代码出现意图模糊的问题。

但是这里出现了一个遗漏的问题,就是停止播放后,播放器会如何工作。

上面代码状态迁移表

二、状态模式

上面的代码已经很清晰了,但是当状态和事件增加后,onEvent函数就会变得非常庞大,这是因为该函数的代码行数与状态和事件数量的乘积成正比。另外 switch 语句中的条件分支也会变得非常复杂,代码会变得难以阅读和理解。

面向对象可以帮助我们将复杂的条件分支转变为多态。当代码中有条件分支时,使用多态性可实现面向对象的状态模式。


#include "cdplayer.h"
typedef struct State
{
    const struct State *(* const stop)(const struct State * pThis);
    const struct State *(* const palyOrPause)(const struct State * pThis);
}State;

static const State * pCurrentState;

static const State * ignore(const State * pThis);

static const State * startPlay(const State * pThis);

static const State * stopPlay(const State * pThis);

static const State * pausePlay(const State * pThis);

static const State * resumePlay(const State * pThis);

const State IDLE = {
    //停止:按钮按下时处理
    ignore,
    //播放/暂停:按钮按下时处理
    startPlay
};

const State PLAY = {
    //停止:按钮按下时处理
    stopPlay,
    //播放/暂停:按钮按下时处理
    pausePlay
};

const State PAUSE = {
    //停止:按钮按下时处理
    stopPlay,
    //播放/暂停:按钮按下时处理
    resumePlay
};


void  initialize(){
    pCurrentState = &IDLE;
  
}

void onStop()
{
    pCurrentState = pCurrentState->stop(pCurrentState);

}

void onPlayOrPause()
{
    pCurrentState = pCurrentState->palyOrPause(pCurrentState);

}

static const State * ignore(const State * pThis)
{
    printf("播放状态\n");
    return pCurrentState;
}

static const State * startPlay(const State * pThis)
{
    printf("播放状态\n");
    return  &PLAY;
}

static const State * stopPlay(const State * pThis)
{
    printf("初始状态\n");
    return  &IDLE;
}

static const State * pausePlay(const State * pThis)
{
    printf("暂停状态\n");
    return  &PAUSE;
}

static const State * resumePlay(const State * pThis)
{
    printf("恢复播放状态\n");
    return  &PLAY;
}

void  initialize(void);

void onStop(void);

void onPlayOrPause(void);



外部调用:


   initialize();
    //播放
    onPlayOrPause();
    //暂停
    onPlayOrPause();
    //恢复播放
    onPlayOrPause();
    //停止
    onStop();


  • 0
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 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、付费专栏及课程。

余额充值