外部レシーバーにメッセージ (テキストなど) をディスパッチする 1 つの状態を持つステート マシンがあります。この状態 ( Dispatchingと呼びましょう) に遷移する前に、 Dispatchingが後でフェッチできるように、前の状態にそのメッセージを格納する場所が必要です。メッセージが 1 つのコンテキストで作成され、別のコンテキストで消費されると、メッセージはヒープ上に作成され、ステート マネージャーオブジェクト (状態、遷移、およびイベント ループを管理する) はそれへの参照/ポインターを保持します。ステート オブジェクトは、ステート マシンがステートを通過するときに作成および破棄されます。各状態は抽象基本クラスを継承しState
ます:
enum StateID
{
STATE_A,
STATE_B,
...
};
class State
{
public:
State(StateID stateID, StateManager& sm) :
stateID_(stateID), sm(sm_){}
virtual ~State(){};
virtual StateID HandleEvent(const Event& e) = 0;
StateID id() const {return stateID_;}
protected:
StateID stateID_;
StateManager& sm_;
};
次の状態へのデータの受け渡しを一般的なものにするために、私はStateData (ある状態から次の状態へ渡される情報の一部) のアイデアを思いつきました。これは動的メモリに格納され、ステート マネージャーはそれへの参照を保持して、各状態がアクセスできるようにします。さまざまなタイプのデータがさまざまな状態に渡される可能性があるため、StateDataを特定の状態ごとに特化した抽象基本クラスにすることができます。
struct StateData
{
virtual ~StateData() = 0;
};
struct StateAData : public StateData
{
int n_;
StateAData(int n) : n_(n){}
};
struct StateBData : public StateData
{
std::string str_;
StateBData(const std::string& str) : str_(str){}
};
...
class StateManager
{
boost::scoped_ptr<State> pCurrState_;
boost::scoped_ptr<StateData> pStateData_;
...
public:
void runEventLoop()
{
while(true)
{
...
//get event from a queue
...
StateID nextStateID = pCurrState_->HandleEvent(e);
if(nextStateID == pCurrState_->id())
continue;
pCurrState_.reset(0);
switch(nextStateID)
{
case STATE_A:
pCurrState_.reset(new StateA(*this));
break;
case STATE_B:
pCurrState_.reset(new StateB(*this));
break;
case STATE_C:
pCurrState_.reset(new StateC(*this));
break;
...
}
}
}
...
};
class StateA
{
public:
StateA(StateManager& sm) : State(STATE_A, sm){}
StateID HandleEvent(const Event& e)
{
switch(e.ID)
{
case EVENT_1:
{
StateAData* pData = reinterpret_cast<StateAData*>(stateMachine_.pStateData_.get());
// do something with data, e.g. use it for transition logic
if(pData->n_ % 2)
{
stateMachine_.pStateData_.reset(new StateBData("Hello from StateA"));
return STATE_B;
}
else
{
...
}
break;
}
...
}
}
...
}
次の行に落とし穴があります。
stateMachine_.pStateData_.reset(new StateBData("Hello from StateA"));
return STATE_B;
遷移ロジックが変更され、ここから に移動する必要がある場合、開発者は のタイプをにSTATE_C
変更するのを忘れる可能性があります。StateBData
StateCData
stateMachine_.pStateData_.reset(new StateBData("Hello from StateA"));
return STATE_C;
StateC
...にキャストしようとするとStateData
、望ましくない動作につながる可能性がありStateCData
ます。
これを回避する方法は?作成されたオブジェクトのタイプと返された列挙値の一致を強制する方法は?
ええ、このコードは悪臭を放ちます。これは、2 つの情報を使用enum
し、型自体ではなく状態型を区別するために使用した結果です。HandleEvent
この返さStateXData
れた型に応じて (次の状態に関する情報を保持するため)、State Manager は (RTTI を使用して) に遷移する次の状態を決定しX
ますが、私はこの解決策が好きではありません。
もう 1 つのアイデアは、次の状態のインスタンスを作成し、そのデータをそのコンストラクターに渡すことですが、このアプローチでは、前の状態が破棄される前に 1 つの状態が作成されるため、状態マシンの設計が損なわれます。