State machine events and object composition

Hi all,
I have a project in which I designed a state machine class by composition of a whole bunch of features that are in the project

1
2
3
4
5
6
7
8
9
10
11
12
13
class MasterFSM
{
public:
    MasterFSM()
    ~MasterFSM() {}

private:
    ArrayCircularBuffer _eventBuffer;

    Sound _beeper;
    UI    _ui;
    NtpRtc _clock;
}


So, in my project, I created a header file called events in which I simply created an enum with all the events that can occur during the operation

1
2
3
4
5
6
7
8
enum event_t
{
    EV_NOTHING = 0,
    // wifi msgs
    EV_START_SENSORS_CMD,
    EV_STOP_SENSORS_CMD
    // etc..
}


Now, I used an enum because I liked that I didn't have to explicitly set number codes to each events, but the way I proceed to call those events from internal objects in the FSM is a bit too convoluted...

In the State machine constructor, I call functions of each internal object that can trigger events, and pass them the events that they should call, and a callback function that they can call with the occuring event codes. ex:
1
2
3
MasterFSM() {
_Sensor.setEvents(EV_PATIENT_GOING_LEFT, EV_PATIENT_GOING_RIGHT, callbackfct);
}

Then, the state machine has a polling fct repeatedly called, which is a big switch case and executes whatever events were called.

I feel like it looked nice at first, but as the internal objects of the FSM grew bigger, and events may be added, it just doesnt scale. plus, some internal objects have themselves internal objects that may need to trigger events, so I just find myself creating an endless amount of setters and getters for those event codes.

I could have gone with global defined macros like it is done in most C firmware,
and I guess it would have been solved, but it doesn't really sound very Cplusplussy...
Is there a clean way to do this, or I am just overthinking it?
Thanks a lot!
Use a look up table (key: event, mapped: handler) perhaps?

Something along these lines:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#include <iostream>
#include <algorithm>
#include <map>
#include <functional>

enum event_t { EV_NOTHING = 0, EV_START_SENSORS_CMD, EV_STOP_SENSORS_CMD, /* etc. */ } ;

struct state_machine
{
    using event_handler_t = std::function< bool(event_t) > ; // return true if handled
    void register_handler( event_t event, event_handler_t handler ) { handler_map[event] = handler ; }

    bool handle_event( event_t event )
    {
        auto iter = handler_map.find(event) ;
        if( iter == handler_map.end() ) return false ;
        else return iter->second(event) ; // call handler, return its result
    }

    std::map< event_t, event_handler_t > handler_map ;
};


int main()
{
    struct X
    {
        bool nothing_handler( [[maybe_unused]] event_t nothing ) const { std::cout << "X::nothing_handler\n" ; return true ; }
    };

    X x ;

    state_machine sm ;
    // register two event handlers
    sm.register_handler( EV_NOTHING, std::bind( &X::nothing_handler, std::addressof(x), std::placeholders::_1 ) ) ;
    sm.register_handler( EV_START_SENSORS_CMD, []( [[maybe_unused]] event_t start )  { std::cout << "starting sensors now\n" ; return true ; } ) ;

    // handle some events
    sm.handle_event(EV_START_SENSORS_CMD) ;
    sm.handle_event(EV_NOTHING) ;
    std::cout << "handled? " << std::boolalpha << sm.handle_event(EV_STOP_SENSORS_CMD) << '\n' ; // false (unhandled)
}

http://coliru.stacked-crooked.com/a/e99119b31119486d

If more than one object could potentially handle the event, use a chain of responsibility:
https://sourcemaking.com/design_patterns/chain_of_responsibility
Is there a clean way to do this

I think this is the wrong question!

Your job is not to produce "clean code", but to optimize the objective quality of your product within the constraints imposed on it: correctness, performance, usefulness, and cost. Not "code cleanliness".

The "cleanliness" of your codebase is completely incidental. If your users are aware of it, they don't care; and if your programmers care, they shouldn't, except as far as it impacts their efforts to measurably improve the program.

Your code is only a means to an end. Do not make it a primary goal.

If you need more convincing, continuing to expend effort on improving the code instead of making actual measurable improvements should quite obviously be a waste of time.

Further, attempting to engineer for code quality is immediately doomed because it is completely subjective.

Finally, even if it could be measured, attempting to optimize it would fall afoul of Goodhart's law.
'Clean code', or flexible design in general, is of concern to the the programmers who have to maintain the code; evolve it over a period of time.

Correctness, performance, usefulness, and cost are of interest to the 'end-users' of the software.

Both are important; we can't ignore either.
Cleanliness and flexible design in general can't be ignored, but developing flexible and clean designs is not the primary goal of software development. Design flexibility and "code cleanliness" should be addressed if doing so seems to be the next needed step along the path to the best possible product.

I believe that many programmers expend effort on "cleaning" their code even when their product would clearly be better off if they directed their efforts differently.
Last edited on
Two things stand out to me here. First, you talk about an FSM, but you're describing an event processor. Are you trying to shoehorn one into another? You need figure this out first.

Second, you have the design inside out. The event processor (or FSM) should not need to worry about the specifics of the events that it handles. All it should know about is the event handler functions that it needs to call.

If it's an event processor, then you probably need a collection of events, so one event can generate another, or several, and their handlers will get called in some priority order.
> First, you talk about an FSM, but you're describing an event processor.
> Are you trying to shoehorn one into another?

Coupling a state machine with an event handler is not unusual. There are very many real-life scenarios where changes of state can be triggered by the arrival of external events.
Topic archived. No new replies allowed.