C++设计模式 - 状态模式

2022-01-18 16:00:18 浏览数 (2)

状态模式

允许一个对象在其内部状态改变时改变它的行为。对象看起来似乎修改了它的类。 --百度百科

简单地说,状态模式就是状态机设计。其主要用于同一个请求,不同条件下执行不同的功能,作用等同于if...else。

意义

在实际编码工作中,经常会遇到一些特殊的场景与普通场景产生的结果不同,逼迫我们不得不使用if来规避。随着时间的推移,这些if使用的越来越多,导致代码晦涩难懂,变成“一坨”,不易维护。此时,我们就应当分析这些特殊的场景,是在哪些特殊条件下产生,能否与普通场景分离,在不影响主干流程的基础上将此场景增加进去。在搞清楚这些条件后,就可以引进状态机进行代码的重构。

场景

电梯在运行过程中,随时都有上电梯、下电梯和等待电梯的需求。这些需求,在电梯空闲、上行或者下行时又会产生不同的动作。例如:

  • 假设电梯处于2楼且空闲状态,遇到1楼需要乘坐电梯。此时电梯应该下行至1楼。
  • 假设乘客在2楼上电梯,并在电梯内按下5楼,此时遇到1楼乘坐电梯。此时电梯应先上行至5楼,再下行至一楼。

同样的需求,由于电梯运行的状态不同,导致产生不同的动作。那么,如果用代码来实现电梯的运行?

分析

按照往常的编码习惯,通常会写一个电梯运行线程,在线程增加各种if用于处理不同的场景。如此设计会存在一些隐患:

  • 所有场景都混在一起,导致思路混乱,容易遗漏特殊场景。
  • 不能及时发现部分场景产生冲突,导致预期之外的行为Bug。
  • if大量使用,导致很难理清代码运行流程,难以维护。

此时,引用状态机就能够很清晰的描述这些场景:

  • 先理清楚电梯的运行状态,无非分为:上行、下行、空闲和故障。
  • 再搞清楚电梯会遇到哪些需求,主要是:当前楼层上预约乘坐电梯、当前楼层下预约乘坐电梯和下电梯。
  • 最后整理出不同状态下遇到这些需求的处理方式以及电梯状态的切换条件,就能够构建出所有场景。

状态图

  • ① 开机运行
  • ② 在当前楼层之上楼层存在需求
  • ③ 当前不存在乘坐需求
  • ④ 当前楼层之上不存在需求,楼层之下存在需求
  • ⑤ 当前楼层之下不存在需求,楼层之上存在需求
  • ⑥ 当前楼层下存在需求
  • ⑦ 不存在乘坐需求
  • ⑧ 电梯主动切换停止状态
  • ⑨ 电梯主动切换空闲状态

编码

由于代码量较多,本篇仅贴出部分核心的代码片段:

状态表

代码语言:txt复制
CElevatorSrv::mStateTable[] =
{
    { LEV1_ANY,         LEV2_ANY,   SIG_ID_POWER_ON,        &CElevatorSrv::MsgRespondInit},
    { LEV1_DOOR_OPEN,   LEV2_IDLE,  SIG_ID_POWER_OFF,       &CElevatorSrv::MsgRespondShutdown},
    { LEV1_DOOR_CLOSE,  LEV2_IDLE,  SIG_ID_TAKE_UP_ORDER,   &CElevatorSrv::MsgRespondOrderUpIdle},
    { LEV1_ANY,         LEV2_ANY,   SIG_ID_TAKE_UP_ORDER,   &CElevatorSrv::MsgRespondOrderUp},
    { LEV1_DOOR_CLOSE,  LEV2_IDLE,  SIG_ID_TAKE_DOWN_ORDER, &CElevatorSrv::MsgRespondOrderDownIdle},
    { LEV1_ANY,         LEV2_ANY,   SIG_ID_TAKE_DOWN_ORDER, &CElevatorSrv::MsgRespondOrderDown},
    { LEV1_ANY,         LEV2_ANY,   SIG_ID_ARRIVE_FLOOR,    &CElevatorSrv::MsgRespondArriveFloor},
    { LEV1_ANY,         LEV2_IDLE,  SIG_ID_EXIT,            &CElevatorSrv::MsgRespondExit},
    { LEV1_ANY,         LEV2_ANY,   SIG_ID_ANY,             &CElevatorSrv::MsgRespondIgnore}
};

:由于在实际场景中,电梯仅允许关门时才可运行。因此这里将开门、关门作为一级状态,电梯运行状态作为二级状态。

消息处理函数

代码语言:txt复制
void CElevatorSrv::ProcessMsg(SMsgPacket *pMsg)
{
    if (!IsStart()) {
        ELEVATORSRV_LOGE("Elevator not start!n");
        return;
    }

    if (pMsg == nullptr) {
        ELEVATORSRV_LOGE("pMsg is nullptr!n");
        return;
    }

    int index = 0;
    EElevatorDoorState curLev1State = GetLev1State();
    EElevatorRunState  curLev2State = GetLev2State();
    EMsgType msgId = pMsg->type;

    ELEVATORSRV_LOGD("Get Msg: 0x%xn", msgId);
    // loop: 遍历状态表,进入与状态匹配的入口
    do
    {
        if (   (   (mStateTable[index].lev1State  == curLev1State)
                || (mStateTable[index].lev1State  == LEV1_ANY)
               )
            && (   (mStateTable[index].lev2State  == curLev2State)
                || (mStateTable[index].lev2State  == LEV2_ANY)
               )
            && (   (mStateTable[index].msgId      == msgId)
                || (mStateTable[index].msgId      == SIG_ID_ANY)
               )
           )
        {
            (this->*(mStateTable[index].callback))(pMsg);
                break;
        }
        index  ;
    } while (1);
}

通过查表的方式,进入匹配当前状态的响应函数。

总结

  • 状态模式的实现方法难度不大,主要是编程的思想的上升。将状态与需求绑定,不仅能够实现统一需求的不同响应方式,还能实现某些状态下不响应指定需求。场景清晰,思路明确。
  • 当需要增加状态或者需求时,只需在表内增加即可,而无需修改已有的逻辑,符合开闭原则
  • 状态模式的使用,可以使关注点仅放在当前状态遇到需求产生的完整流程。无需考虑其他状态的影响。
  • 将需求与响应解耦,还能够实现通信管理。例如,不同进程间的需求响应,可以将两者通信设计为不涉及业务的跨进程通信,从而实现通信代码的可复用。
  • 总的来说,状态模式是一种非常实用的设计模式。不仅是从代码上还有设计思路上,减轻设计师对复杂业务的整理工作。相同的完美!

0 人点赞