Alex
Alex

Reputation: 10126

Avoid public `SetState()` interface in state pattern implementation in C++

The state pattern

Class diagram

itself is really nice pattern for implementing state machines because it allows to encapsulate state transitions logic in states themselves and adding a new state is actually becomes easier because you need to make changes only in relevant states.

But, it is usually avoided in description how should states be changed.

If you implement state change logic in Context then whole the point of pattern is missed, but if you implement state change logic in states, that means you need to set a new state in Context.

The most common way is to add the public method to Context SetState() and pass reference to Context to the state object, so it will be able to set a new state, but essentially it will allow the user to change state outside the state machine.

To avoid it I came to the following solutions:

class IContext {
     public:
         virtual void SetState(unique_ptr<IState> newState) = 0;
}

class Context : public IContext {
     private:
         virtual void SetState(unique_ptr<IState> newState) override { ... };
}

But in general changing the method scope in derived class doesn't look really good.

Is there another way to hide this interface (friend class is not an option because it requires to change the Context class for each state being added)?

Upvotes: 1

Views: 1014

Answers (3)

Lennard Beers
Lennard Beers

Reputation: 1

Not sure how much this helps, but I just implemented a sample state machine in C# that uses the observer pattern and a tiny bit of reflection to get a very clean and encapsulated implementation of the state pattern.

Context.cs:

using System;
using System.Collections.Generic;
using System.Linq;

public class Context
{
    State State { get; set; }
    List<State> States { get; }

    public Context()
    {
        States = new()
        {
            new HappyState(),
            new SadState(),  
        };

        SetState<HappyState>();
    }

    void DoSomething() => State?.DoSomething();

    string ReturnSomething() => State?.ReturnSomething();

    void SetState<StateType>() where StateType : State => SetState(typeof(StateType));

    void SetState(Type stateType)
    {
        if (!stateType.IsSubclassOf(typeof(State))) return;

        var nextState = States.Where(e => e.GetType() == stateType).First();
        if (nextState is null) return;

        if (State is not null)
        {
            State?.ExitState();
            State.ChangeRequested -= OnChangeRequested;
        }

        State = nextState;
        State.ChangeRequested += OnChangeRequested;
        State.EnterState();
    }

    void OnChangeRequested(Type stateType) => SetState(stateType);
}

State.cs:

using System;

public abstract class State
{
    public event Action<Type> ChangeRequested;
    protected void SetState<StateType>() where StateType : State
    {
        ChangeRequested?.Invoke(typeof(StateType));
    }

    public virtual void EnterState() { }
    public virtual void ExitState() { }
    public virtual void DoSomething() { }
    public virtual string ReturnSomething() => "";
}

You can then use this Syntax in either the Context or any State

SetState<HappyState>();

Link to Repository

Upvotes: 0

Al TT
Al TT

Reputation: 1

The goal of the state pattern is to hide/encapsulate different implementations from the caller.However, caller only needs to know what type of implementation it needs.

Upvotes: 0

Micha&#235;l Roy
Micha&#235;l Roy

Reputation: 6471

You could consider having the handler handle()returning the next state...

class IState {
     public:
         virtual unique_ptr<IState> handle(Context&) = 0;
};

class StateA : public IState {
     private:
         // presented inline for simplicity, but should be in .cpp
         // because of circular dependency.
         //
         virtual unique_ptr<IState> handle(Context& ctx) override
         {
             //...
             if (/*...*/)
               return make_unique(StateB{});

             //...  including other state switch..

             return { nullptr };  // returning null indicates no state change, 
                                  // returning unique_ptr<>(this) is not really an option.
         }
};

Upvotes: 1

Related Questions