Where to place an actor within the state pattern?

Viewed 78

I have problems finding the right place for an actor and a timer used in a state machine.

I found some inspiration from this site about the state pattern: State Design Pattern in Modern C++ and created a small example:

Simple door state machine

There might be more transitions possible but I kept it short and simple.

class Door 
{  
    void open() {}
    void close() {}
};

Events:

class EventOpenDoor
{
public:
    OpenDoor(Door* door) : m_pDoor(door) {}
    Door* m_pDoor;
};

class EventOpenDoorTemporary
{
public:
    EventOpenDoorTemporary(Door* door) : m_pDoor(door) {}
    Door* m_pDoor;
};

class EventOpenDoorTimeout
{
public:
    EventOpenDoorTimeout(Door* door) : m_pDoor(door) {}
    Door* m_pDoor;
};

class EventCloseDoor
{
public:
    EventCloseDoor(Door* door) : m_pDoor(door) {}
    Door* m_pDoor;
};

using Event = std::variant<EventOpenDoor,
                           EventOpenDoorTemporary,
                           EventOpenDoorTimeout,
                           EventCloseDoor>;

States:

class StateClosed {};

class StateOpen {};

class StateTemporaryOpen {};

using State = std::variant<StateClosed,
                           StateOpen,
                           StateTemporaryOpen>;

Transitions (not complete):

struct Transitions {

    std::optional<State> operator()(StateClosed &s, const EventOpenDoor &e) {
        if (e.m_pDoor)
        {
            e.m_pDoor->open();
        }
        auto newState = StateOpen{};
        return newState;
    }

    std::optional<State> operator()(StateClosed &s, const EventOpenDoorTemporary &e) {
        if (e.m_pDoor)
        {
            e.m_pDoor->open();
            **// start timer here?**
        }
        auto newState = StateOpen{};
        return newState;
    }

    std::optional<State> operator()(StateTemporaryOpen &s, const EventOpenDoorTimeout &e) {
        if (e.m_pDoor)
        {
            e.m_pDoor->close();
        }
        auto newState = StateOpen{};
        return newState;
    }

    std::optional<State> operator()(StateTemporaryOpen &s, const EventOpenDoor &e) {
        if (e.m_pDoor)
        {
            e.m_pDoor->open();
            **// stop active timer here?**
        }
        auto newState = StateOpen{};
        return newState;
    }

    /* --- default ---------------- */
    template <typename State_t, typename Event_t>
    std::optional<State> operator()(State_t &s, const Event_t &e) const {
        // "Unknown transition!";
        return std::nullopt;
    }
};

Door controller:

template <typename StateVariant, typename EventVariant, typename Transitions>
class DoorController {
    StateVariant m_curr_state;
    void dispatch(const EventVariant &Event)
    {
        std::optional<StateVariant> new_state = visit(Transitions{this}, m_curr_state, Event);
        if (new_state)
        {
            m_curr_state = *move(new_state);
        }
    }

    public:
    template <typename... Events>
    void handle(Events... e) 
{ (dispatch(e), ...); }

    void setState(StateVariant s)
    {
        m_curr_state = s;
    }
};

The events can be triggered by a client which holds an instance to the "DoorController"

door_controller->handle(EventOpenDoor{door*});

In the events I pass a pointer to the door itself so it's available in the transitions. The door is operated within the transitons only.

I have problems now with modelling the 20s timeout/timer. Where to have such a timer, which triggers the transition to close the door? Having a timer within the door instance means, I have a circular dependency, because in case of a timeout it needs to call "handle()" of the "door_controller".

I can break the circular dependency with a forward declarations. But is there a better solution?

Maybe I have modelled it not well. I'm open to improving suggetions. Thanks a lot!

1 Answers

This isn't going to be the best answer, but I have more questions than answers.

Some of your choices seem odd. I presume there's a complicated reason why you're storing state based on a variant rather than using an enum class State{}, for instance.

I also get nervous when I see raw pointers in modern C++. I'd feel a whole lot better with smart pointers.

When I've done state machines, the events I can handle always subclass from a common Event class -- or I might even just use a single class and give it as many distinct data fields are required for the things that I need to handle. It's a little odd that you use unrelated classes and depend on a dispatch method. Does that even work? Aren't you pushing objects onto an event queue? How do you end up calling that dispatch method with random objects?

You don't show your event loop, but maybe you have a state machine without an event loop. Is it a state machine then? Or maybe you didn't show it. Maybe you can have a state machine without an event loop, but I thought the two concepts were tied together.

Related