状态模式

又是一种优化过于臃肿 if...else... 语句的方法~

意图

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

动机

想象一个场景, 以马路边行人过马路红绿灯为例子, 红灯亮 30 秒后亮 3 秒黄灯,然后变成亮 20 秒绿灯, 亮完绿灯又是 3 秒黄灯接着红灯这样的循环

如果按照 if...else... 的逻辑, 先判断当前状态是红灯还是绿灯然后决定下一步亮哪一种, 甚至当前是黄灯的时候, 还需要判断前一次是哪种,才能决定, 当然为了方便可以将黄灯分为 红转绿的黄灯, 绿转红的黄灯 2种, 简单用伪代码展示一下逻辑

void switchLight() {
    if (当前状态 == 绿灯) {
        当前状态 = 绿转红的黄灯;
        // 亮黄灯 3 秒;
        return 3;
    } else if ( 当前状态 == 红灯) {
        当前状态 = 红转绿的黄灯;
        // 亮黄灯 3 秒;C++
        return 3;
    } else if ( 当前状态 == 绿转红的黄灯 ){
        当前状态 == 红灯;
        // 亮红灯 30 秒;
        return 30;
    } else {
        当前状态 == 绿灯;
        // 亮绿灯 20 秒;
        return 20;
    }
}

调用部分的伪代码大概

while (true) {
    int wait = switchLight();
    sleep(wait);
}

假设现在有个场景, 凌晨基本没有行人通过这个红绿灯, 最大程度的保证机动车通行, 那么红绿灯希望可以一直都是红灯, 或者由原来 30 秒的红灯, 变成 180 秒, 同时绿灯的时间也从 20 秒变成 10 秒, 此时我们就需要改 switchLight(), 随着实际开发需求的不停改变, 最后可能会出现巨大的条件语句, 导致代码不够清晰, 并且难以修改和扩展

而状态模式的意图就是将 决定状态转移 的逻辑从 if 或者 switch 中剥离出来, 将它们分布到 State 的各个子类中, 将每一个 状态转换 和动作封装到一个类中

UML 图

uml

  • Context (环境)
    • 定义客户端感兴趣的接口
    • 维护一个 ConcreteState 子类的实例,这个实例定义当前状态
  • State (状态)
    • 定义一个接口以封装与 Context 的一个特定状态相关的行为
  • ConcreteState subclasses (具体状态子类)
    • 每一个子类实现一个与 Context 的一个状态相关的行为

简单说一下这些类协作流程

  • Context 是客户端使用的主要接口, 客户可用状态对象来配置一个 Context, 一旦一个 Context 配置完毕, 它的客户不再需要直接与状态对象打交道
  • Context 将与状态相关的请求委托给当前的 ConcreteState 对象处理。
  • ContextConcreteState 都可以决定状态改变的规则, 以及在何种条件下进行状态转换

状态模式的意义就是让客户端在整个过程中, 只操作 Context, 状态的改变交给 State 类及其子类, 那么 Context 就需要增加一个接口, 让 State 对象显式地设定 Context 的当前状态, 但是这样意味 State 子类至少拥有一个其他子类的信息, 这将在各个子类之间产生了实现的依赖

应用场景

  • 一个对象的行为取决于它的状态, 并且它必须在运行时刻根据状态改变它的行为。
  • 一个操作中含有庞大的多分支的条件语句,且这些分支依赖于该对象的状态。这个状态通常用一个或多个枚举常量表示。通常 , 有多个操作包含这一相同的条件结构。状态模式将每一个条件分支放入一个独立的类中。这使得你可以根据对象自身的情况将对象的状态作为一个对象,这一对象可以不依赖于其他对象而独立变化。

举个例子

这里就使用红绿灯这个例子, 接下来主要看一下各个部分的实现细节, 首先是 State 这个基类

// state.h 
class Context;

class State
{
public:
    State();
    virtual ~State();

    int Handle(Context *context);
    void setNextState(State *state);

private:
    virtual int doSomething() =0;

private:
    State *m_nextState;
};


// state.cpp
#include "state.h"
#include "context.h"

State::State() {}
State::~State() {}

int State::Handle(Context *context)
{
    int duration = doSomething();
    context->setState(m_nextState);
    return duration;
}

void State::setNextState(State *state)
{
    m_nextState = state;
}

接着是红绿灯各个状态的子类实现

#include "state.h"
#include <iostream>
using namespace std;

class RedState : public State
{
private:
    int doSomething() {
        cout << "RedLight ";
        return 30;
    }
};

class GreenState : public State
{
private:
    int doSomething() {
        cout << "GreenLight ";
        return 20;
    }
};

class YellowState : public State
{
private:
    int doSomething() {
        cout << "YellowLight ";
        return 3;
    }
};

然后是供客户端的调用的 Context

// context.h
#include <list>

class State;

class Context
{
public:
    Context();
    ~Context();

    int Handle();

private:
    friend class State;
    void setState(State *state);

private:
    std::list<State *> m_allStates;
    State *m_currentState;
};

// context.cpp
#include "context.h"
#include "state.h"

#include "concretestate.h"

template <typename Iterator>
void DeleteElement(Iterator begin, Iterator end)
{
    while (begin != end) {
        delete *begin;
        ++begin;
    }
}

template <typename Container>
inline void DeleteAll(const Container &c)
{
    DeleteElement(c.begin(), c.end());
}

Context::Context()
{
    RedState *red_state = new RedState;
    GreenState *green_state = new GreenState;
    YellowState *rtog_state = new YellowState;
    YellowState *gtor_state = new YellowState;

    red_state->setNextState(rtog_state);
    green_state->setNextState(gtor_state);
    rtog_state->setNextState(green_state);
    gtor_state->setNextState(red_state);

    m_allStates.push_back(red_state);
    m_allStates.push_back(green_state);
    m_allStates.push_back(rtog_state);
    m_allStates.push_back(gtor_state);

    m_currentState = red_state;
}

Context::~Context()
{
    DeleteAll(m_allStates);
}

void Context::setState(State *state)
{
    m_currentState = state;
}

int Context::Handle()
{
    return m_currentState->Handle(this);
}

最后是真正的调用环节 main 部分

#include "context.h"
#include "windows.h"

#include <iostream>
using namespace std;

int main()
{
    Context trafficLights;

    while (true) {
        int wait = trafficLights.Handle();
        cout << "Lighting : " << wait  << " second" << endl;
        // Sleep(wait*1000);
        Sleep(1000);
    }

    return 0;
}

最后的运行结果:

RedLight Lighting : 30 second
YellowLight Lighting : 3 second
GreenLight Lighting : 20 second
YellowLight Lighting : 3 second
RedLight Lighting : 30 second
YellowLight Lighting : 3 second
GreenLight Lighting : 20 second
YellowLight Lighting : 3 second
...

这个例子的实现在一些实现细节上是有一定取舍的,我着重介绍一下实现细节

谁定义状态转换

例子代码中, 状态转换的规则是在 Context 在创建 ConcreteState 后通过 StatesetNextState() 函数将状态转换的流程定义了下来, 最后 State 这个基类通过 ContextsetState() 函数来显式的转换

简单点说:Context 拥有所有子类的信息, 并且定义了状态转换的逻辑,统筹了 ConcreteState, 虽然实际上还是由 ConcreteState 来切换,但是避免了一个 Sate 子类至少拥有一个其他子类的信息,从而子类间不存在实现依赖的情况

例子中的状态转换 A->B->C 很简单,实际情况中可能并不是 A->B 这种情况,可能 A 在一些情况下可以变成 B, 在另一些情况下变成 C, 或者 State 存在多种行为,比如 HandleA()HandleB()HandleC, 在 A 状态下

  • 执行 HandleA() 报错
  • 执行 HandleB() , 切换成 B 状态
  • 执行 HandleA() , 切换成 C 状态

那么就不能简单的通过 setNextState() 这种方式(当然这是我的实现思路),所以举的例子的实现方式可以进一步改进, 比如 Context 提供返回它创建的 ConcreteState 的接口,代码大致是

void State::Handle(Context *context)
{
    if (condition) {
        dosomethingA();
        context->setState(context->getConcreteStateA());
    } else {
        doSomethingB();
        context->setState(context->getConcreteStateB());
    }
}

而且状态设置的实例对象可以交给 State 子类去创建, 也就是类似于下面这样 ConcreteStateA 切换到 ConcreteStateB,这样 Context 只需要知道初始状态,其它的都可以不必关心, 但是 ConcreteStateA 就需要知道 ConcreteStateB 对象,也就存在了实现依赖

// class ConcreteStateA 
void ConcreteStateA::Handle(Context *context)
{
    doSomething();
    context->setState(new ConcreteStateB());
}

管理 State 对象

例子中 State 对象们被管理在 Contextstd::list<State *> m_allStates; 对象中, 并不会随着切换一个新的 State 对象而销毁,这样的好处是,Context 预先一次付清创建各个状态对象的开销,如果状态切换改变很频繁,这种方式挺好的

当将要进入的状态在运行时是不可知的, 并且上下文不经常改变状态时, 可仅当需要 State 对象时才创建它们并随后销毁它们, 有点类似于 setState(new ConcreteStateB()) 的形式,不过一定要防止内存泄漏的问题

当然管理 State 对象方法还是很多的,比如使用 享元模式(Flyweight) 或者 单例模式(Singleton)

总结

封装 基于状态 的行为,并将行为委托到当前状态,这个思路还是很有意思的~