I'm implementing a state pattern in C++ with a context and several states. Each state implements its transition. Here's a simplified version of what that design could look like:
class Context;
class State
{
public:
State(Context* ctx);
virtual void transition() = 0;
protected:
Context* context;
}
class Context{
public:
void transition();
void setState(State* next);
private:
State* current;
}
class StateA : public State
{
public:
StateA(Context* ctx);
void transition() override;
}
class StateB : public State
{
public:
StateB(Context* ctx);
void transition() override;
}
Context::setState(State* newState)
{
current = newState;
}
Context::transition()
{
current->transition();
}
StateA::transition()
{
context->setState(new StateB(context));
}
StateB::transition()
{
// noop
}
Now, I need to add a new state transition that requires an integer parameter, like this:
StateB::transition(int a) // This doesn't match the base class!
{
if (a > 5)
{
context->setState(new StateA(context));
}
}
I want to avoid rewriting the entire state pattern for this new requirement since 90% of the code remains the same. How can I modify my current implementation to accommodate state transitions with additional parameters while reusing the existing code?
1 Answer 1
The extra parameter will only make sense in a layer of the code where the class StateB
"lives". Let me call this "application layer".
Your state pattern implementation, especially the classes Context
and State
, however, lives inside some "framework layer", which does not know anything about the specific StateB
and it's requirements for an extra parameter.
The straightforward solution here is to make the parameter a
a member of the class StateB
, which has to be initialized before Context::transition()
is called. The initialization has to happen somewhere inside the application layer. You have basically the following options for this:
Pass the initial parameter value in the StateB constructor - that requires each constructor call to look like
new StateB(context,initialA)
. That's feasible when initialA is available at all places and at the time wherenew StateB
is called. In your example, this happens inside the classStateA
, and it is not clear ifinitialA
is accessable in that class, or a the point in time when StateB is constructed.Provide an extra setter method
StateB::setA(int initialA)
and call it somewhere in the application layer where initialA is known, but, beforeContext::transition
. That may require detection of whenContext::current
is of typeStateB
and a downcast. It also has some risk to be forgotten to be called and it givesStateB
some mutable state.
If none of these options works for you, you can also try to extend the framework in general:
Create an abstraction for "potentially extra parameters" in the framework layer, for example, an
ExtraParameters
class, and extend the signature ofState::transition
andContext::transition
by thisExtraParameters
. All states which don't need this parameter for a transition should ignore it, butStateB::transition
can extract the parametera
from it.This will have the biggest impact on your current framework implementation, but it could be sensible when different
State
subclasses require different forms of extra parameters. There are some different possible design alternatives forExtraParameters
, you have to think which one makes most sense for your case.
What to choose depends on your real-world context, how many State types require what kind of parameters, and where and when the parameters are become available in the system, and where or how often they change - that is something only you know, since you left no clue for us about these details in the question.
-
Having a setter in StateB works. My problem is finding an elegant way to keep a handle on StateB at my application level, but this is a different problem so I will mark it as resolved.Emile Papillon-Corbeil– Emile Papillon-Corbeil2024年07月12日 12:19:41 +00:00Commented Jul 12, 2024 at 12:19
-
Attempt #1 godbolt.org/z/TecP3645b Note: A lot of complexity comes from the fact that I cannot store pointers that will bind to other values after construction (for reasons related to the middleware) - so I had to provide an enum value to determine the current state with a way to convert it to state objects.Emile Papillon-Corbeil– Emile Papillon-Corbeil2024年07月12日 12:27:51 +00:00Commented Jul 12, 2024 at 12:27
Explore related questions
See similar questions with these tags.
a
.