我有一个使用State设计模式实现的C ++状态机。每个状态都实现为上下文类的嵌套友元类。
class Context {
public:
/* The Context class' user calls process1() to get it to perform an action */
void process1();
private:
class IState;
void switchState( IState *newState );
class IState {
virtual void doProcess( Context &context ) = 0;
};
class StateA : public Context::IState {
void doProcess( Context &context );
};
friend class StateA;
class StateB : public Context::IState {
void doProcess( Context &context );
};
friend class StateB;
.
.
.
class StateJ : public Context::IState {
void doProcess( Context &context );
};
friend class StateJ;
};
目前,当调用Context::StateA
时,状态机的成功迭代从Context::StateJ
运行到Context::process1()
但是某些状态包含内部逻辑以确定是否循环回更早州。所以典型的执行方式如下:
StateA
StateB
StateC
StateD
StateE
StateC
StateD
StateE
StateF
StateG
StateH
StateI
StateJ
通过将数据存储在上下文对象中,当前正由相应的状态本身实现确定下一状态的内部逻辑。我现在需要做的是添加一个Context::process2()
选项,它在执行状态的顺序上有很大不同。当然,这可以使用在上下文对象中设置的标志来完成,但我想知道是否有更好的方法来实现它;甚至可以使用这种方法来重写Context::process2()
中的状态切换处理。
Visitor design pattern可能会成功,但我不确定它是否打算用于实现状态机。使用访问者时,process1()
可以包含状态执行顺序的所有逻辑,然后只按该顺序调用每个状态。同样,process2()
将处理它自己的所有逻辑。
修改
对于那些回复说我应该创建一个单独的状态机的人,我希望避免这种情况的原因是因为第二个状态机使用的状态的代码与第一个状态机的代码相同;只有进展是不同的。
第二个状态机将经历以下状态转换:
StateA
StateB
StateC
StateJ
所以我试图消除重复的代码。
答案 0 :(得分:3)
我假设在您的代码中,当转换到其他内容时,各个州会调用switchState
。像这样:
void StateA::doProcess(Context& context) {
context.switchState(new StateB()); // NOTE: potential leak!
}
是这样的吗?
如果是这样,您可以考虑的一件事是让状态返回过渡对象,它抽象地表示状态图中的控制点。然后让Context运行一个循环来执行状态,检索生成的转换,并将转换映射到适当的下一个状态,以用于您拥有的任何进程。可以为您拥有的每个process
方法设置不同的转换映射。
优点:
缺点:
编辑:示例代码为http://pastebin.com/eBauP060。
答案 1 :(得分:3)
您可能希望使用一种技术或几种技术的组合来减少重复。例如:
如果在每个状态事件处理程序方法中有一些复杂的处理,请将此处理移至Context。
甚至更好,创建第三类X
(我不知道如何调用它。也许是Driver?有什么建议吗?或者这个类应该叫做Context,而Context应该叫做SomeStateMachine?)哪个将包含所有改变它的常用数据和方法。
然后在每个州的doProcess
方法中,你只会:
检查输入
调用X
更改状态,可能基于上一步的返回值。
这样,您的状态机只负责调用正确的方法并切换到正确的状态。其余的都将在其他地方。
而不是像这样切换状态:
context.switchState(new StateB());
使你的州无国籍 - 没有实例领域,只有方法。然后在Context中创建一组静态实例,每个IState子类一个。这样你就可以避免分配所有这些对象了:
context.switchState(&Context::StateBInstance);
现在更好了。您可以在上下文中创建IState实例中的字段。在状态机构建期间将决定哪个实例。这可以让你在运行时连接你的状态机。
void Context::Context()
{
theStateThatComesAfterA = &StateBInstance;
// [...]
}
void AnotherContext::AnotherContext()
{
theStateThatComesAfterA = &StateCInstance;
// [...]
}
void StateA::doProcess(Context& context)
{
context.DoSomething();
// [...]
context.switchState(context.theStateThatComesAfterA);
}
最后一点说明。不要忘记这种概括性的东西。 记得。有时代码中的相似性表示应该消除的重复。 但是,我们常常看起来太过刻板,并且基于两个概念的纯粹意外相似性而概括。 然后,当在设计的演变过程中,这些概念开始出现分歧时,由于无关事物的强耦合,很难改变代码。
一般来说,这是设计师的电话。您需要查看您的特定要求和约束,并选择正确的技术。这就是为什么这种状态机设计模式(以及许多其他模式)是一种设计模式,而不是一个库类。
答案 2 :(得分:1)
感谢你努力做一些困难的事,做对了,让它干嘛!
正如tomekszpakowicz所指出的,如果状态之间有不同的转换,那么你有不同的状态机。你提到的每个“过程”听起来都像是一个不同的状态机。
如果在状态中指定转换,则每台计算机都需要单独的状态。如果要在状态中重用代码,则从状态中提取常用功能并进入可重用状态表示。
如果指定状态之外的转换,那么状态本身可以从一台机器重用到另一台机器;你只需要为每个状态指定不同的转换。您可能会发现必须动态构建状态机 - 也就是说,在编译时,您有一堆可供机器使用的状态和转换。在机器的构建例程中,您可以通过创建状态和转换的实例来连接所有这些。 (所有这些都很难,imo。)
答案 3 :(得分:0)
此代码段未编译。
private:
class IState;
void switchState( IState *newState );
class IState {
virtual void doProcess( Context &context ) = 0;
};
顺便说一句,您可以依赖模板专业化来实现各种状态的doProcess()实现:
class Context
{
public:
class IState;
void switchState( IState *newState );
class IState {
virtual void doProcess( Context &context ) = 0;
};
template<typename T>
class StateA : public Context::IState {
public:
void doProcess( Context &context );
};
template<typename T>
class StateB : public Context::IState {
public:
void doProcess( Context &context );
};
template<typename T>
class StateJ : public Context::IState {
public:
void doProcess( Context &context );
};
public:
/* The Context class' user calls process1() to get it to perform an action */
template<typename T>
void process()
{
StateA<T> a;
a.doProcess(*this);
}
};
struct Process1
{
};
struct Process2
{
};
void main(int, char **)
{
Context c;
c.process<Process1>();
c.process<Process2>();
}
此外,如果你保持相同数量的状态,你会得到一个通用的状态机。