如何以面向对象的思想设计有限状态机

状态机的概念

有限状态机又称有限状态自动机,简称状态机,是表示有限个状态以及在这些状态之间的转移和动做等行为的数学计算模型,用英文缩写也被简称为 FSM。
FSM 会响应“事件”而改变状态,当事件发生时,就会调用一个函数,并且 FSM 会执行动做产生输出,所执行的动做会由于当前系统的状态和输入的事件不一样而不一样。
算法

问题背景

为了更好地描述状态机的应用,这里用一个地铁站的闸机为背景,简单叙述一下闸机的工做流程:
一般闸机默认是关闭的,当闸机检测到有效的卡片信息后,打开闸机,当乘客经过后,关闭闸机;若是有人非法经过,那么闸机就会产生报警,若是闸机已经打开,而乘客仍然在刷卡,那么闸机将会显示票价和余额,并在屏幕输出“请经过,谢谢”。
在了解了闸机的工做流程以后,咱们就能够画出闸机的状态图,状态图以下:
闸机状态图
在上图中,线条上面的字表示的是:闸机输入事件/闸机执行动做,方框内表示的是闸机的状态。
除了使用状态图来表示系统的工做流程外,咱们也能够采用状态表的方式来表示系统的工做流程,状态表以下所示:




编程

起始状态 事件 结束状态 动做
Locked card Unlocked unlock
Locked pass Locked alarm
Unlocked card Unlocked thankyou
Unlocked pass Locked lock

经过上述咱们已经知道闸机的工做流程了,接下来咱们来看具体的实现。数据结构

代码实现

嵌套的 switch 语句

使用嵌套的 switch 语句是最为直接的办法,也是最容易想的方法,第一层 switch 用于状态管理,第二层 switch 用于管理各个状态下的各个事件。代码实现能够用下述伪代码来实现:架构

switch(当前状态)
{
case LOCKED 状态:
	switch(事件):
	{
	case card 事件:
		 切换至 UNLOCKED 状态;
		 执行 unlock 动做;
		 break;
	case pass 事件:
		执行 alarm 动做;
		break;
	}
	break;
case UNLOCKED 状态:
	switch(事件):
	{
		case card 事件:
			执行 thankyou 动做;
			break;
		case pass 事件:
			切换至 LOCKED 状态;
			执行 lock 动做;
			break;
	}
	break;
}

上述代码虽然很直观,可是状态和事件都出如今一个处理函数中,对于一个大型的 FSM 中,可能存在大量的状态和事件,那么代码量将是很是冗长的。为了解决这个问题,能够采用状态转移表的方法来处理。框架

状态转移表

为了减小代码的长度,可使用查表法,将各个信息存放于一个表中,根据事件和状态查找表项,找到须要执行的动做以及即将转换的状态。函数

typedef struct _transition_t
{
	状态;
	事件;
	转换为新的状态;
	执行的动做;
}transition_t;

transition_t transitions[] = {
	{LOCKED 状态,card 事件,状态转换为UNLOCKED,unlock动做},
	{LOCKED 状态,pass 事件,状态保持为LOCKED,alarm 动做},
	{UNLOCKED 状态,card 事件,状态转换为 UNLOCKED,thankyou动做},
	{UNLOCKED 状态,pass 事件,状态转换为 LOCKED,lock 动做}
};

for (int i = 0;i < sizeof(transition)/sizeof(transition[0]);i++)
{
	if (当前状态 == transition[i].状态 && 事件 == transition[i].事件)
	{
		切换状态为:transition[i].转换为新的状态;
		执行动做:transition[i].执行的动做;
		break;
	}
}

从上述咱们能够看到若是要往状态机中添加新的流程,那么只须要往状态表中添加东西就能够了,也就是说整个状态机的维护及管理只须要把重心放到状态转移表的维护中就能够了,从代码量也能够看出来,采用状态转移表的方法相比于第一种方法也大大地缩减了代码量,并且也更容易维护。
可是对于状态转移表来讲,缺点也是显而易见的,对于大型的 FSM 来讲,遍历状态转移表须要花费大量的时间,从而影响代码的执行效率。
那要怎样设计代码量少,又不须要以遍历状态转移表的形式从而花费大量时间的状态机呢?这个时候就须要以面向对象的思想来设计有限状态机。

学习

面向对象法设计状态机

面向对象基本概念

以面向对象的思想实现的状态机,大量涉及了对于函数指针的用法,必须对这个概念比较熟悉优化

上述所提到了两个设计方法都是基于面向过程的一种设计思想,面向过程编程(POP)是一种以过程为中心的编程思想,以正在发生的事件为主要目标,指导开发者利用算法做为基本构建块构建复杂系统。
即将所要介绍的面向对象编程(OOP)是利用类和对象做为基本构建块,所以分解系统时,能够从算法开始,也能够从对象开始,而后利用所获得的结构做为框架构建系统。
提到面向对象编程,那天然绕不开面向对象的三个基本特征:

this

  • 封装:隐藏对象的属性和实现细节,仅仅对外公开接口
  • 继承:使用现有类的全部功能,并在无需从新编写原来的类的状况下对这些功能进行扩展,C 语言使用 struct 的特性实现继承
  • 多态性:使用相同的方法,根据对象的类型调用不一样的处理函数。

上述对于面向对象的三个基本特征作了一个简单的介绍,封装和继承的概念都都比较清晰,多态性这个特色可能会有所迷惑,在这里笔者用在书中看到一个例子来解释多态性,例子是这样的:
要求画一个形状,这个形状是多是圆形,矩形,星形,不管是什么图形,其共性都是须要调用一个画的方法来进行绘制,绘制的形状能够经过函数指针调用各自的绘图代码绘制,这就是多态的意义,根据对象的类型调用不一样的处理函数。
在介绍了上述很基本的概念以后,咱们来看状态机的设计。

spa

实现细节

咱们由浅入深地来思考这个问题,首先咱们能够想到把闸机当作一个对象,那么这个这个对象的职责就是处理 card 事件(刷卡)和 pass 事件(经过闸机),闸机会根据当前的状态执行不一样的动做,也就有了以下的代码:

enum {LOCKED,UNLOCKED};/*枚举各个状态*/

/*定义闸机类*/
typedef struct _turnstile
{
    int state;
    void (*card)(struct _turnstile *p_this);
    void (*pass)(struct _turnstile *p_this);
}turnstile_t;

/* 闸机 card 事件 */
void turnstile_card(turnstile_t *p_this)
{
    if (p_this->state == LOCKED)
    {
        /* 切换至解锁状态 */
        /* 执行unlock动做,调用 unlock 函数 */
    }
    else
    {
        /* 执行 thank you 动做,调用 thank you 函数 */
    }
}

/* 闸机 pass 事件*/
void turnstile_pass(turnstile_t *p_this)
{
    if (p_this->state == LOCKED)
    {
        /* 执行 alarm 动做,调用 alarm 函数*/
    }
    else
    {
        /* 状态切换至锁闭状态 */
        /* 执行 lock 动做,调用 lock 函数 */
    }
}

上述代码的思想实现的有限状态机相比于前两种不须要进行大量的遍历,也不会致使代码量的冗长,看似已经比较完美了,可是咱们再仔细想一想,若是此时状态更改了,那 turnstile_card 函数和 turnstile_pass 函数都要更改,也就是说事件和状态存在着耦合,这与“高内聚,低耦合”的思想所违背,也就是说若是咱们要继续优化代码,那须要对事件和状态进行解耦。

状态和事件解耦

将事件与状态相分离,从而使得各个状态的事件处理函数很是的单一,所以在这里须要定义一个状态类:

typedef struct _turnstile_state_t
{
    void (*card)(void);  /* card 事件处理函数 */
    void (*pass)(void);  /* pass 事件处理函数 */
}turnstile_state_t;

在定义了状态类以后,咱们就可使用状态类建立 lock 和 unlock 的实例并初始化。

turnstile_state_t locked_state = {locked_card,locked_pass};
turnstile_state_t unlocked_state = {unlocked_card,unlocked_pass};

在这里须要补充一下上述初始化项里函数里的具体实现。

void locked_card(void)
{
    /* 状态切换至解锁状态 */
    /* 执行 unlock 动做 ,调用 unlock 函数 */
}

void locked_pass(void)
{
    /* 执行 alarm 动做,调用 alarm 函数 */
}

void unlocked_card(void)
{
    /* 执行 thank you 动做,调用 thank you 函数 */ 
}

void unlocked_pass(void)
{
    /* 状态切换至锁闭状态 */
    /* 执行 lock 动做,调用 lock 函数 */
}

这样,也就实现了状态与事件的解耦,闸机再也不须要判断当前的状态,而是直接调用不一样状态提供的 card() 和 pass() 方法。定义了状态类以后,因为闸机是整个系统的中心,咱们还须要定义闸机类,因为 turnstile_state_t 中只存在方法,并不存在属性,那么咱们能够这样来定义闸机类:

typedef struct _turnstile_t
{
    turnstile_state_t *p_state;
}turnstile_t;

到这里,咱们已经定义了闸机类,闸机状态类,以及闸机状态类实例,他们之间的关系以下图所示:
在这里插入图片描述
经过图中咱们也能够看到闸机类是继承于闸机状态类的,locked_state 和 unlocked_state 实例是由闸机状态类派生而来的,那最底下的那个箭头是为何呢?这是在后面须要讲到的对于闸机状态转换的处理,在获取输入事件调用具体的方法进行处理后,咱们须要修改闸机类的p_state,因此也就有了这个箭头。
相比于最开始定义的闸机类,这个显得更加简洁了,同时 p_state 能够指向相应的状态对象,从而调用相应的事件处理函数。
在定义了一个闸机类以后,就能够经过闸机类定义一个闸机实例:



turnstile_t turnstile;

而后经过函数进行初始化:

void turnstile_init(turnstile_t *p_this)
{
    p_this->p_state = &locked_state;
}

整个系统闸机做为中心,进而须要定义闸机类的事件处理方法,定义方法以下:

/* 闸机 card 事件*/
void turnstile_card(turnstile_t *p_this)
{
    p_this->p_state->card();
}

/* 闸机 pass 事件 */
void turnstile_pass(turnstile_t *p_this)
{
    p_this->p_state->pass();
}

到这里,咱们回顾前文所述,咱们已经可以对闸机进行初始化并使得闸机根据不一样的状态执行不一样的处理函数了,再回顾整个闸机的工做流程,咱们发现闸机在工做的时候会涉及到从 locked 状态到 unlocked 状态的相互变化,也就是状态的转移,所以状态转移函数能够这样实现:

void turnstile_state_set(turnstile_t *p_this,turnstile_state_t *p_new_state)
{
    p_this->p_state = p_new_state;
}

而状态的转移是在事件处理以后进行变化的。那么咱们能够这样修改处理函数,这里用输出语句替代闸机动做执行函数:

void locked_card(turnstile_t *p_turnstile)
{
    turnstile_state_set(p_turnstile,&unlocked_state);
    printf("unlock\n");   /* 执行 unlock 动做 */
}

void locked_pass(turnstile_t *p_turnstile)
{
    printf("alarm\n");   /* 执行 alarm 动做*/
}

void unlocked_card(turnstile_t *p_turnstile)
{
    printf("thankyou\n"); /* 执行 thank you 动做*/
}

void unlocked_pass(turnstile_t *p_turnstile)
{
    turnstile_state_set(p_turnstile,&locked_state);
    printf("lock\n");     /* 执行 lock 动做 */
}

既然处理函数都发生了变化,那么闸机状态类也应该发生更改,更改以下:

typedef struct _turnstile_state_t
{
    void (*card)(turnstile_t *p_turnstile);
    void (*pass)(turnstile_t *p_turnstile);
}turnstile_state_t;

可是回顾以前咱们给出的闸机类和闸机状态类的关系,闸机类是继承于闸机状态类的,也就是说先有的闸机状态类后有的闸机类,可是这里却在闸机状态类的方法中使用了闸机类的参数,其实这样也是可行的,须要提早对闸机类进行处理,总的闸机类状态类定义以下:

#ifndef __TURNSTILE_H__
#define __TURNSTILE_H__

struct _turnstile_t;
typedef struct _turnstile_t turnstile_t;

typedef struct _turnstile_state_t
{
    void (*card)(turnstile_t *p_turnstile);
    void (*pass)(turnstile_t *p_turnstile);
}turnstile_state_t;

typedef struct _turnstile_t
{
    turnstile_state_t *p_state;
}turnstile_t;

void turnstile_init(turnstile_t *p_this);    /* 闸机初始化 */
void turnstile_card(turnstile_t *p_this);    /* 闸机 card 事件处理 */
void turnstile_pass(turnstile_t *p_this);    /* 闸机 pass 事件处理 */

#endif

上述就是全部的关于状态机的相关定义了,下面经过上述的定义实现状态机的实现:

#include <stdio.h>
#include <turnstile.h>

int main(void)
{
    int event;
    turnstile_t turnstile;          /* 闸机实例 */
    turnstile_init(&turnstile);     /* 初始化闸机为锁闭状态 */

    while(1)
    {
        scanf("%d",&event);
        switch(event)
        {
        case 0:
            turnstile_card(&turnstile);
            break;
        case 1:
            turnstile_pass(&turnstile);
            break;
        default:
            exit(0);
        }
    }

上述代码运行结果以下:
在这里插入图片描述

结论

以上即是笔者关于状态机的所有总结,讲述了面向过程和面向对象两种实现方法,虽然从篇幅上看面向对象的方法要更为复杂,可是代码的执行效率以及长度都要优于面向过程的方法,因此了解面向对象的程序设计方法是颇有必要的。

这篇文章是在笔者学习了《程序设计与数据结构》周立功版后的本身的理解,该书的PDF版能够从立功科技官网的周立功专栏中获取。

下面给出书籍和文章状态机代码汇总的连接:
程序设计与数据结构
连接:https://pan.baidu.com/s/17ZH7Si1f_9My7BulLs8AVA
提取码:x1im
FSM:
连接:https://pan.baidu.com/s/1qO-Dy6bHukBRGxxQ1-KGJA
提取码:vyn2





最后,若是您以为个人文章对您有所帮助,欢迎关注笔者的我的公众号:wenzi嵌入式软件
在这里插入图片描述

相关文章
相关标签/搜索