有限状态机与状态模式

本文探讨了if-else结构在面对复杂状态逻辑时的问题,如状态变化复杂可能导致遗漏和高耦合。通过引入有限状态机(FSM)和状态模式,可以改善代码的可维护性和扩展性。状态模式将每个状态封装为独立对象,减少代码耦合,允许在运行时根据对象状态透明地改变行为。文中以MP3播放器为例,展示了如何应用状态模式和状态机来优化代码,使得在需求变更时能够更方便地添加新状态。
摘要由CSDN通过智能技术生成

下面是我们编程时常见的一个例子

enum Network_State {
    Net_Close,
    Net_Open,
    Net_Connect,
};

void operation(int state) {
    if (state == Net_Close) {
        // 。。。。
    } else if (state == Net_Open) {
        // 。。。。
    } else if (state == Net_Connect) {
        
    }
}

这种代码结构特别特别常见,根据当前状态执行不同的操作,符合大多数人直接的编程习惯

如果这时需要在Network_State中添加新的状态Net_Wait,我们一般的做法也是在operation中修改代码,添加一个分支处理

以设计模式的角度讲,这违背了开闭原则(对扩展开放,对更改封闭;类模块应该是可扩展的,但是不可修改的)!,因为它在需求变更时,需要不断地修改内部源代码,而好的设计,应该是以添加对象或接口的方式实现扩展。

如果觉得用设计模式描述问题太深奥,那么我们换点接地气的方式,先就if-else代码结构,慢慢展开分析问题。


if-else结构的问题

使用if-else,或switch-case的结构,一般来说没有很大的问题,简单直接。但有一个比较麻烦的地方在于,当分支情况变得复杂,而且代码时常会面临需求变化时,这时我们再使用if-else会有以下几个问题:

  • 分支增加,状态变化更加复杂,我们无法面面俱到,可能会遗漏部分情况;
  • 需求变化,某处代码改动会影响到其它部分,代码之间的耦合性太强,复用率低,维护难度大。

对于第一个问题,我们常用的解决方法是使用状态机,画状态迁移图和状态转移表,然后根据状态迁移图进行编程。对于第二个问题,我们应用状态模式,对代码结构进行优化解耦,将状态迁移内化隐藏,消除代码中的if-else。


有限状态机(FSM)

有限状态机可以将复杂的逻辑简化为有限个稳定状态,在稳定状态中判断事件。

image-20210722152525372image-20210722152552162

左图是一个文件的状态迁移图。右图是其相应的状态转换表,其中第一行蓝色部分是稳定的状态,而第一列黄色部分是可能发生的事件。

有限状态机的设计,可以使得复杂的逻辑代码更加清晰严谨,更加容易维护。

参考:产品之术:一目了然的状态机图


状态模式

在软件构建过程中,某些对象的状态如果改变,其行为也会随之而发生变化,比如文档处于只读状态,其运行的行为和读写状态支持的行为就可能不同。

如何在运行时,根据对象的状态来透明地更改对象的行为?而不会为对象操作和状态转化之间引入紧耦合?(这句话没读懂也不打紧,我们先继续往下走。)

状态模式要点:

State模式将所有与一个特定状态相关的行为都放入一个state的子类对象中,在对象状态切换时,切换相应的对象,但同时维持state接口,这样实现了具体操作与状态转换之间的解耦。

为不同的状态引入不同的对象使得状态转换变得更加明确,而且可以保证不会出现状态不一致的情况,因为转换是原子性的 — 即要么彻底转换,要么不转换。


状态机结合状态模式实例

​ 设计一个简易的MP3播放器,要求两个功能按键,如按下space键切换播放/暂停,按下Esc键停止播放。
image-20210722112123000

简单应用状态机进行编程

代码中有大量的switch-case和if-else。

void onEvent(EventCode ec)
{
  switch (state)
  {
  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;
  }

应用状态模式优化代码结构
#include "stdio.h"

/*
 * 有限状态机,结合状态模式实例:一个简易的mp3播放器框架
 * 支持两个功能按键,如esc键停止播放,space键切换播放/暂停
 */


// 1、定义状态接口
typedef struct _state {				// 某状态下
    void (* stop) ();				// 停止键的处理函数
    void (* play_or_pause) ();		// 播放/暂停键的处理函数
} mp3_state;

// 2. 定义系统的当前状态指针
mp3_state *p_cur_state;

// 3. 定义具体的状态,根据状态迁移图来具体实现功能和状态切换
void play_null();
void play_start();
void play_pause();
void play_resume();
void play_stop();

// 初始化态,不能stop,可以play
mp3_state IDLE = {
    play_null,      // 等价于.stop = play_null,
    play_start,
};

mp3_state PLAY = {
    play_stop,
    play_pause,
};

mp3_state PAUSE = {
    play_stop,
    play_resume,
};


void play_null() {
    // 空函数
}
void play_start() {
    printf("开始播放音乐\n");
    // .......                  // 执行操作
    p_cur_state = &PLAY;        // 切换状态
}
void play_stop() {
    printf("停止播放音乐\n");
    p_cur_state = &IDLE;
}
void play_pause() {
    printf("暂停播放音乐\n");
    p_cur_state = &PAUSE;
}
void play_resume() {
    printf("恢复播放音乐\n");
    p_cur_state = &PLAY;
}


// 4. 定义上下文操作接口,主程序只关心当前状态,不关心状态之间是如何变化的
void onStop(void) {
    p_cur_state->stop();
}
void onPlayOrPause(void) {
    p_cur_state->play_or_pause();
}

// 5. 指定系统的起始状态
void mp3player_init() {
    p_cur_state = &IDLE;
}

// 主程序通过上下文操作接口来控制系统状态变化
int main(int argc, char const* argv[])
{
    mp3player_init();       // 当前为IDLE态

    mp3_state context = {   // 用context隔离内部p_cur_state的迁移
        onStop,
        onPlayOrPause,
    };
                              // cur_state next_state 
    context.play_or_pause();  // IDEL态, 进入播放
    context.play_or_pause();  // PLAY态, 暂停播放
    context.play_or_pause();  // PAUSE态,恢复播放
    context.stop();           // PLAY态, 停止播放
    return 0;
}
image-20210722115048639

需求变更时,如何扩展添加一个新状态?

​创建一个新的状态对象,然后实现其状态下对应按键的处理函数,最后将函数实现赋给对象内部的函数指针。

参考:c语言设计模式–状态模式(状态机)


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值