通过单例模式在C++中实现状态机?

3

我认为实现状态机的好方法是使用单例模式。例如,可以像这样:

class A
{

private:
    friend class State;
    State* _state;
    void change_state(State* state) { _state = state; }
};

class State
{
    virtual void action(A *a) = 0;
private:
    void change_state(A *a, State *state) { a->change_state(state); }
};

class StateA : public State
{
public:
    static State* get_instance()
    {
        static State *state = new StateA;
        return state;
    }
    virtual void action(A *a) { change_state(a, StateB::get_instance(); }
};

class StateB : public State
{
public:
    ...
    virtual void action(A *a) { change_state(a, StateA::get_instance(); }
};

我的问题是:我已经读了很多有关单例模式非常糟糕的文章。如果不使用单例模式实现状态机模式,每次更改状态都必须调用new,那么对于那些不喜欢单例模式的人来说,你会如何实现状态机模式?

5个回答

5
我认为单例模式在这里并不合适。单例适用于表示抽象实体或物理对象,其实只有一个副本。借用Java中的例子,特定程序实例运行时只有一个运行环境。单例适用于表示这些对象,因为它们让整个程序能够命名和引用它,同时保留封装性并允许多个可能的后端。
鉴于此,我不同意使用单例作为状态机的最佳方法。如果您将其实现为单例,则表示始终存在该状态机的一个副本。但是,如果我想要并行运行两个状态机怎么办?或者根本没有状态机?如果您的状态机是单例的,我无法进行任何这些操作,因为整个程序确实只使用一个状态机。
现在,根据您如何使用状态机,也许它是合适的。如果状态机控制程序的整体执行,则可能是个好主意。例如,如果您正在开发视频游戏,并希望状态机控制您是否在菜单中、聊天区域中或玩游戏,则完全可以使用单例状态机,因为任何时候程序都只有一个逻辑状态。但是,从您的问题中,我无法推断出这是否是情况。
至于如何在没有单例的情况下实现状态机,您可能希望使状态机对象分配自己的每个状态的副本并构建转换表(如果需要显式状态对象),或者只有一个巨大的开关语句和一个控制您所处状态的枚举值。如果您只有一个状态机实例,这与当前版本一样有效,如果您有多个实例,则允许您在每个状态中存储本地信息,而不会污染可能被程序其他部分读取的全局状态副本。

1
如果我想要同时运行两个状态机怎么办?状态机不是单例 - 状态 是单例,但在上述设计中,状态机是A。我们可以实例化任意数量的状态机。 - Steve Jessop
1
@Steve Jessop- 我的担忧是,如果状态机状态是需要某种状态的单例(例如,如果它是网络协议的控制器,则状态可能需要存储IP地址等),那么将状态作为单例会阻止我使用多个状态机而不会相互干扰。也许我应该更清楚地表达这一点...尽管我承认在意识到这一细节之前,我已经写了很多内容。 :-) - templatetypedef

4
您的StateAStateB类没有数据成员。其他状态也不会有可修改的数据成员,因为如果有这样的成员,那么该状态将在同时运行的不同A实例之间共享,这是很奇怪的。
因此,您的单例已经避免了模式的一半(全局可变状态)问题。实际上,在对设计进行小修改的情况下,您可以用函数替换状态类;用函数指针替换指向它们实例的指针;并用当前函数指针调用代替虚拟调用action。如果有人因使用单例而给您带来很多麻烦,但您确信自己的设计是正确的,那么您可以进行这个小改动,并观察他们是否注意到他们的“更正”对设计没有产生任何重大影响。
然而,单例的另一半问题仍未得到解决,即固定依赖关系。使用单例时,无法模拟StateB以便单独测试StateA,或者在要引入与当前库中相同但StateA转移到StateC而不是StateB的新状态机时引入灵活性。您可能认为这是个问题,也可能不认为是个问题。如果您认为这是个问题,那么您需要使每个状态更具可配置性,而不是使每个状态成为单例。
例如,您可以为每个状态分配一个标识符(字符串或枚举成员),并在A类的某个地方注册一个State*。然后,StateA可以将其翻转到用于表示“此状态机中的状态B”的任何状态对象,而不是翻转到StateB的单例实例。这可以是某些实例的测试模拟。您仍然需要针对每个状态更改调用一次new,但不需要每次状态更改都调用一次new
实际上,这仍然是类A的策略模式,就像您的设计一样。但是,与其只有一种策略来推动状态机向前移动,并不断替换它作为状态更改,我们有每个状态通过的一个策略,所有策略都具有相同的接口。在C++中的另一个选项是使用基于策略的设计,而不是策略。然后,每个状态由一个类处理(作为一个模板参数提供),而不是一个对象(在运行时设置)。因此,您的状态机的行为在编译时固定(与当前设计相同),但可以通过更改模板参数进行配置,而不是以某种方式更改或替换StateB类。然后,您根本不需要调用new - 在状态机中创建每个状态的单个实例作为数据成员,使用其中一个的指针来表示当前状态,并像以前一样进行虚拟调用。基于策略的设计通常不需要虚拟调用,因为通常各个策略是完全独立的,而在这里它们实现了一个共同的接口,我们在运行时在它们之间进行选择。

所有这些假设A知道一组有限的状态。这可能不现实(例如,A可能代表一个通用可编程状态机,应接受任意数量的任意状态)。在这种情况下,您需要一种建立状态的方法:首先创建StateA的实例和StateB的实例。由于每个状态都有一个退出路径,因此每个状态对象应该有一个指向新状态的数据成员指针。因此,在创建状态之后,将StateA实例的“下一个状态”设置为StateB实例,并反之亦然。最后,将A的当前状态数据成员设置为StateA实例,并开始运行它。请注意,当您执行此操作时,您正在创建一个循环依赖图,因此为了避免内存泄漏,您可能需要采取特殊的资源处理措施,超出引用计数范围。


0
在你的代码中,你没有将状态与状态机关联起来(假设类A是状态机)。这些信息会传递到操作方法中。因此,如果你有两个类A的实例(即两个状态机),那么你可能会导致一个状态更新错误的状态机。
如果你这样做是为了避免重复调用new和delete以提高速度,那么这可能是一种过早的优化。更好的解决方案是,如果你可以证明使用new和delete太慢/引起其他问题(例如内存碎片化),则在State基类中定义一个operator new/delete,从自己的内存池中分配。
以下是我目前正在使用的状态机的伪代码:
class StateMachine
{
public:
   SetState (State state) { next_state = state; }
   ProcessMessage (Message message)
   {
     current_state->ProcessMessage (message);
     if (next_state)
     {
       delete current_state;
       current_state = next_state;
       next_state = 0;
     }
   }
private:
   State current_state, next_state;
}

class State
{
public:
   State (StateMachine owner) { m_owner = owner; }
   virtual ProcessMessage (Message message) = 0;
   void *operator new (size_t size) // allocator
   {
     return memory from local memory pool
   }
   void operator delete (void *memory) // deallocator
   {
     put memory back into memory pool
   }
protected:
   StateMachine m_owner;
};

class StateA : State
{
public:
  StateA (StateMachine owner) : State (owner) {}
  ProcessMessage (Message message)
  {
    m_owner->SetState (new StateB (m_owner));
  }
}

内存池可以是一系列内存块的数组,每个内存块都足够大,可以容纳任何状态,并带有一对列表,一个用于分配的块,另一个用于未分配的块。分配块然后成为从未分配列表中删除块并将其添加到已分配列表的过程。释放是相反的过程。我认为这种分配策略的术语是“自由列表”。它非常快,但存在一些浪费内存的问题。

0
一种假设所有状态对象都存在于StateMachine中的方法可能是这样的:
enum StateID
{
   STATE_A,
   STATE_B,
   ...
};

// state changes are triggered by events 
enum EventID
{
   EVENT_1,
   EVENT_2,
   ...
};

// state manager (state machine)
class StateMachine
{
   friend StateA;
   friend StateB;
   ...

public: 
   StateMachine();
   ~StateMachine();
   // state machine receives events from external environment
   void Action(EventID eventID);
private:
   // current state
   State* m_pState;

   // all states
   StateA* m_pStateA;
   StateB* m_pStateB;
   ... 

   void SetState(StateID stateID);       
};

StateMachine::StateMachine()
{
   // create all states
   m_pStateA = new StateA(this, STATE_A);
   m_pStateB = new StateB(this, STATE_B);
   ...

   // set initial state
   m_pState = m_pStateA; 
}

StateMachine::~StateMachine()
{
   delete m_pStateA;
   delete m_pStateB;
   ...
}

void StateMachine::SetState(StateID stateID)
{
   switch(stateID)
   {
   case STATE_A:
      m_pState = m_pStateA;
      break;
   case STATE_B:
      m_pState = m_pStateA;
      break;
   ...
   }
}

void StateMachine::Action(EventID eventID)
{
   // received event is dispatched to current state for processing
   m_pState->Action(eventID);
}

// abstract class
class State
{
public:
   State(StateMachine* pStateMachine, StateID stateID);
   virtual ~State();
   virtual void Action(EventID eventID) = 0;
private:
   StateMachine* m_pStateMachine;
   StateID m_stateID;       
};

class StateA : public State
{
public: 
   StateA(StateMachine* pStateMachine, StateID stateID);    
   void Action(EventID eventID);
};

StateA::StateA(StateMachine* pStateMachine, StateID stateID) : 
   State(pStateMachine, stateID) {...}

void StateA::Action(EventID eventID)
{
   switch(eventID)
   {
   case EVENT_1:
      m_pStateMachine->SetState(STATE_B);
      break;
   case EVENT_2:
      m_pStateMachine->SetState(STATE_C);
      break;
   ...
   }
}

void StateB::Action(EventID eventID)
{
   switch(eventID)
   {
   ...
   case EVENT_2:
      m_pStateMachine->SetState(STATE_A);
      break;
   ...
   }
}

int main()
{
   StateMachine sm;
   // state machine is now in STATE_A

   sm.Action(EVENT_1);
   // state machine is now in STATE_B

   sm.Action(EVENT_2);
   // state machine is now in STATE_A

   return 0;
}

在更复杂的解决方案中,StateMachine 将具有事件队列和事件循环,等待来自队列的事件并将其分派到当前状态。所有耗时操作都应在 StateX::Action(...) 中以独立的(工作)线程运行,以防止阻塞事件循环。

0
我正在考虑的一种设计方法是创建一个状态工厂,它是单例的,这样更多的状态机可以使用工厂生产的状态对象。
但是这个想法让我想到了用享元模式来实现我的状态工厂,这就是我停下来的地方。
基本上,我需要研究将状态对象实现为享元的优势,以及享元设计模式的优势。
我听说过使用这种模式的状态机,但不确定它是否适合我的需求。
无论如何,我正在做一些研究,并偶然发现了这篇文章。只是想分享一下...

单例模式从来不是答案。 - thecoshman
@thecoshman:单例模式不是答案,但有时候单例模式可能是。 - DrP3pp3r

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接