我认为实现状态机的一个好方法是使用单例模式。 例如,它看起来像这样:
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,所以对于那些不喜欢单例的人,你将如何实现状态机模式?
答案 0 :(得分:4)
我不认为单身模式在这里是合适的。单身人士很适合表示真正只有一个副本的抽象实体或物理对象。要从Java窃取示例,只有一个运行时环境,程序的特定实例在其中执行。单例很适合表示这些对象,因为它们使整个程序能够命名和引用它,同时保留封装并允许多个可能的后端。
鉴于此,我不同意单身人士是你的状态机的最佳途径。如果你把它实现为单例,那么你就是说它总是该状态机的一个副本。但是,如果我想让两台状态机并行运行怎么办?或者根本没有国家机器?如果我想要我自己的本地状态机,那么我可以尝试一下,看看它发生了什么?如果您的状态机是单例,我不能做任何这些事情,因为整个程序实际上只使用了一台状态机。
现在,根据您使用状态机的方式,也许这是合适的。如果状态机控制程序的整体执行,那么这可能是个好主意。例如,如果您正在开发视频游戏并希望状态机控制您是在菜单中,还是在聊天区中,还是在玩游戏,那么拥有单例状态机就完全没问题了。任何时候程序只有一个逻辑状态。但是,从你的问题来看,我不能推断出是否是这种情况。
至于如何在没有单例的情况下实现状态机,您可能希望使状态机对象分配其自己的每个状态的副本并构建转换表(如果您需要显式状态对象),或者只是巨型switch语句和一个枚举值控制你所处的状态。如果你有一个状态机实例,这个效率并不比当前版本低,如果你有多个实例,它允许你存储本地信息每个州都没有污染程序其他部分可以读取的状态的全局副本。
答案 1 :(得分:2)
您的StateA
,StateB
类没有数据成员。据推测,其他州也不会有可修改的数据成员,因为如果他们这样做,那么该状态将在A的不同实例之间奇怪地共享,即不同的状态机同时运行。
所以你的单身人士避免了模式问题的一半(全局可变状态)。事实上,只需对设计进行一些小改动,就可以用函数替换状态类;用函数指针替换指向实例的指针;并通过当前函数指针调用虚拟调用action
。如果有人给你使用单身人士很麻烦,但你确信你的设计是正确的,你可以做出这个小小的改变,看看他们是否注意到他们的“修正”对设计没有任何重大影响。
单身人士问题的另一半仍然无法解决,而且这是固定的依赖关系。使用单例,无法模拟StateB以便单独测试StateA,或者当您想要向库引入新的状态机时引入灵活性,这与当前的状态机相同,只是StateA转到StateC而不是国家B您可能会或可能不会考虑这个问题。如果你这样做,那么你需要让事情更具可配置性,而不是让每个州都成为单身人士。
例如,您可以为每个州提供一些标识符(字符串或枚举的成员),并为每个标识符在A类中的某个地方注册State*
。然后不要翻转到单例实例StateB,StateA可以翻转到用于在此状态机中表示“状态B”的任何状态对象。那可能是某些实例的测试模拟。每个机器每个状态仍然会调用new
一次,但每次状态更改时不会调用一次。
实际上,这仍然是您设计中A类的策略模式。但是,不是采用单一策略来向前移动状态机,而是在状态发生变化时不断更换状态机,我们在机器通过的每个状态下都有一个策略,所有策略都具有相同的接口。 C ++中的另一个选项是用于某些用途而不用于其他用途,是使用(一种)基于策略的设计而不是策略。然后每个状态由一个类(作为模板参数提供)而不是一个对象(在运行时设置)处理。因此,状态机的行为在编译时是固定的(如在当前设计中),但可以通过更改模板参数来配置,而不是通过某种方式更改或替换类StateB。然后你根本不需要调用new
- 在状态机中创建每个状态的单个实例,作为数据成员,使用指向其中一个的指针来表示当前状态,并创建一个虚拟像以前一样打电话给它。基于策略的设计通常不需要虚拟调用,因为通常单独的策略是完全独立的,而在这里它们实现了一个公共接口,我们在运行时选择它们。
所有这一切都假设A知道一组有限的状态。这可能不太现实(例如,A可能代表一个应该接受任意数量的任意状态的通用可编程状态机)。在这种情况下,您需要一种方法来构建状态:首先创建StateA的实例和StateB的实例。由于每个状态都有一个出口路径,因此每个状态对象应该有一个数据成员,该成员是指向新状态的指针。因此,创建状态后,将StateA实例设置为StateB实例的“next state”,反之亦然。最后,将A的当前状态数据成员设置为StateA的实例,并开始运行。请注意,执行此操作时,您将创建依赖关系的循环图,因此为了避免内存泄漏,您可能必须采取特殊的资源处理措施,而不是引用计数。
答案 2 :(得分:0)
在您的代码中,您没有将状态与状态所属的状态机相关联(假设A类是状态机)。此信息将传递给action方法。因此,如果您有两个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));
}
}
内存池可以是一块内存块,每个内存块足以容纳任何状态,带有一对列表,一个用于分配的块,另一个用于未分配的块。然后,分配块将成为从未分配的列表中删除块并将其添加到分配的列表的过程。然后,解放是相反的过程。我认为这种分配策略的术语是“免费列表”。它非常快,但有一些浪费的记忆。
答案 3 :(得分: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(...)
中的所有耗时操作都应该在单独的(工作线程)线程中运行,以防止阻塞事件循环。
答案 4 :(得分:0)
我正在考虑的一种设计方法是创建一个单独的状态工厂,这样多个状态机就可以使用工厂生成的状态对象。
但是这个想法让我想到了用flyweight模式实现我的州工厂,这就是我停下来的地方。
基本上,我需要研究将状态对象实现为flyweights的优势,然后研究flyweight设计模式的优点。
我听说这种状态机使用这种类型的模式,但不确定它是否适合我的需要。
无论如何,我正在做一些研究并碰到这篇文章。只是想我会分享......