OPEnSLab-OSU / eDNA-Server

Backend Application for eDNA Genomic Sampler
1 stars 1 forks source link

Decouple state from its connections #12

Closed kawinie closed 4 years ago

kawinie commented 4 years ago

Currently, each state in the state machine defines how it should transition to the next state. While this allows for complex branching and transitioning logic close to the state, it discourages state reuse between multiple state controllers. The proposed API shift the responsibility of transitioning logic to state controller.

// KPStateMachine.hpp
class KPStateMachine {
    virtual void next(int exit_code = 0);  // state with multiple branches would call this method with different exit code
};
// State.cpp
State1::enter(KPStateMachine & sm) {
    ...
    // or sm.next(), sm.next(1), sm.next(2) ...
    // Each state calls sm.next(..) to give up control of the runtime
    sm.next(0);
}

Subclasses of the KPStateMachine should implement virtual functions like this

// CustomStateController.hpp
class CustomStateController : public KPStateMachine {
public:
    void next(int arg) override {
        if (currentState.name() == StateName::State1) {
            switch(arg) {
            case 0:
                transitionTo(StateName::State2);
                return;
            case 1:
                transitionTo(StateName::State3);
                return;
            default:
                error("Unhandled state transition");
            }
        }
    }
};
kawinie commented 4 years ago

KPStateMachine Update

Here is the final version of the KPStateMachine. I added two new methods: next() and restart() as additional APIs to be called from inside the Enter lifecycle method of a state. By using next(), we are leaving the responsibility of transitioning to a new state up to the StateMachine instance, not the State itself. This allows for code reuse. Example usages can be found at feature/state-controller branch. registerState() of KPStateMachine has optional third argument that accepts a functor.

#pragma once
#include <KPSubject.hpp>
#include <KPStateMachineObserver.hpp>
#include <unordered_map>

class KPState;
class KPStateMachine : public KPComponent, public KPSubject<KPStateMachineObserver> {
private:
    using Middleware = std::function<void(int)>;
    std::unordered_map<const char *, KPState *> mapNameToState;
    std::unordered_map<const char *, Middleware> mapNameToMiddleware;
    KPState * currentState = nullptr;

public:
    using KPComponent::KPComponent;

    template <typename T>
    void registerState(T && state, const char * name, Middleware middleware = nullptr) {
        if (mapNameToState.count(name)) {
            halt(TRACE, name, " already exist");
        }

        if (name == nullptr) {
            halt(TRACE, "State must have a name");
        }

        T * copy             = new T{std::forward<T>(state)};
        copy->name           = name;
        mapNameToState[name] = copy;
        if (middleware) {
            mapNameToMiddleware[name] = middleware;
        } else {
            mapNameToMiddleware[name] = [](int code) { halt(TRACE, "Unhandled state transition"); };
        }
    }

    template <typename T>
    void registerState(T && state, const char * name, const char * next_name) {
        registerState(std::forward<T>(state), name,
                      [this, next_name](int code) { transitionTo(next_name); });
    }

    template <typename T>
    T * getState(const char * name) {
        auto c = mapNameToState[name];
        if (c) {
            return static_cast<T *>(c);
        } else {
            return nullptr;
        }
    }

    KPState * getCurrentState() const {
        return currentState;
    }

    void next(int code = 0);
    void restart();
    void transitionTo(const char * name);

protected:
    void setup() override;
    void update() override;
};