也谈状态模式

时间:2022-07-23
本文章向大家介绍也谈状态模式,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。
状态机流转图

看到上面的这个状态转换图,一般来说我们还想不到用状态机模式去解决,因为太简单了,简单得可能几行代码就处理了。

switch-case 大法

通常我们一般会使用下面的代码处理状态机流转的需求,

enum Event {
  EVENT_LOGIN,    // 登录事件
  EVENT_LOGOUT,   // 登出事件
  EVENT_EXPIRED,  // 登录过期事件
  EVENT_UNKNOWN,
};

enum State {
  STATE_START,    // 初始状态
  STATE_LOGIN,    // 登录状态
  STATE_UNKNOWN,
};

struct Context {
  State state_;
};

void SwitchCaseMethod(Event event, Context &context) {
  switch (event) {
    case EVENT_LOGIN:
      // do something for login event
      context.state_ = STATE_LOGIN;
      break;
    case EVENT_LOGOUT:
      // do something for logout event
      context.state_ = STATE_START;
      break;
    case EVENT_EXPIRED:
      // do something for expired event
      context.state_ = STATE_START;
      break;
    default:
      // ignore
      break;
  }
}

但是如果状态机的复杂度增加到下面这样,

闹钟状态转换图

使用 switch-case 写出来的很可能就会像面条一样了,

意大利面条

表驱动

当然我们如果做出适当的抽象,可以写出类似于下面的代码,

enum Event {
  EVENT_LOGIN,    // 登录事件
  EVENT_LOGOUT,   // 登出事件
  EVENT_EXPIRED,  // 登录过期事件
  EVENT_UNKNOWN,
};

enum State {
  STATE_START,    // 初始状态
  STATE_LOGIN,    // 登录状态
  STATE_UNKNOWN,
};

struct Context {
  State state_;
};

typedef void (*EventHandler)(Context &context);

void LoginHandler(Context &context) {}
void LogoutHandler(Context &context) {}
void ExpiredHandler(Context &context) {}

#define TABLE_SIZE 255

EventHandler EventHandlerTable[TABLE_SIZE] = {0};

void TableDriveInit() {
  EventHandlerTable[EVENT_LOGIN] = LoginHandler;
  EventHandlerTable[EVENT_LOGOUT] = LogoutHandler;
  EventHandlerTable[EVENT_EXPIRED] = ExpiredHandler;
}

void TableDriveMethod(Event event, Context &context) {
  if (event >= TABLE_SIZE) {
    return;
  }
  auto handler = EventHandlerTable[event];
  if (NULL != handler) {
    handler(context);
  }
}

代码的可维护性提高了好多,看起来很干净。但是如果事件增加到几十个,状态也增加到几十个,上面的方式也让人吃不消了,那么我们需要引出状态模式解决我们的问题了。

状态模式

状态模式的定义可以在这里找到,可以发现状态模式似乎和我们的处理逻辑有些差异,那就是抽象的状态方法只有一个 doAction,和我们 login、logout 和 expired 有些差异,那么我们只需要增加几个方法即可,就像下面这样,

enum Event {
  EVENT_LOGIN,    // 登录事件
  EVENT_LOGOUT,   // 登出事件
  EVENT_EXPIRED,  // 登录过期事件
  EVENT_UNKNOWN,
};

enum State {
  STATE_START,    // 初始状态
  STATE_LOGIN,    // 登录状态
  STATE_UNKNOWN,
};

struct Context {
  State state_;
};

class AbstractState {
 public:
  virtual void onLogin(Context &context) {}
  virtual void onLogout(Context &context) {}
  virtual void onExpired(Context &context) {}
};

class StartState : public AbstractState {
 public:
  virtual void onLogin(Context &context) {
    // do something and transfer to new state
    context.state_ = STATE_LOGIN;
  }
};

class LoginState : public AbstractState {
  virtual void onLogout(Context &context) {
    // do something and transfer to new state
    context.state_ = STATE_START;
  }
  virtual void onExpired(Context &context) {
    // do something and transfer to new state
    context.state_ = STATE_START;
  }
};

这样就能满足在每个状态下只做这个状态感兴趣的事情,其他的都可以忽略,这样我们能够应对大部分场景。

更进一步

上面我们所说的能够的应对大部分场景言外之意就是有些场景还是应对不了,比如事件经常变化的场景就需要改动抽象类,可参考 spring-statemachine 框架,框架把状态、事件和动作的注册进行了抽象,修改时不需要修改基类只需要修改注册的事件、状态和动作。

参考文献

游戏人工智能编程案例精粹,https://book.douban.com/subject/3081930/

代码大全第十八章,https://book.douban.com/subject/1477390//

知乎这个问题下有很多高质量的回答,https://www.zhihu.com/question/31845498

有赞工作流引擎,https://tech.youzan.com/workflow-engine-in-youzan-devops/

spring-statemachine 实践,https://cloud.tencent.com/developer/article/1534194

终极大法,https://book.douban.com/subject/21964984/