3
\$\begingroup\$

I am trying to achieve an event system, which I refer as actions, and would like your ideas about what I come up with so far. I have made research on several design patterns, and several systems used for jobs like this, like Signal & Slots, Observer and command patterns etc. However, I have found several various implementations, which were probably way better than what I have come up with, but I had some hard time trying to understand some points and use cases in most of them. Instead of using something that I can not extend when I need, I decided the come up with my own designs inspired by them. I am currently working on an experimental project, which is only purpose is for me to learn more and more about architectural and functional design in C++. I am not a beginner programmer, but I can say I am somewhat experimental in C++, did not have a lot of background on it. So basically, I am doing trial & error, seeing what happens, and the pros and cons of the system I come up with by using them. So there is no problem about revisions and refactoring, I can do it all day.

Anyways coming back to my system, in the most event handling systems I had searched, there was mostly only 1 logic, where an event is dispatched to subscribed listeners, and it was listener's job to check the desired parameters and act upon it. However, currently what I need is a system, that checks the events itself, on the choices of the listeners and sends them to the listeners only if they are fully interested. What i mean is that, consider this example:

// Dispatcher sends this to all listeners.
onKeyPressEvent.dispatch(pressedKey, Key_A);

// And listener does something like this
onKeyPressEvent.subscribe(myLambdaWithParameter);

And when it receives the event, it checks it. For example, a keypress event is SENT to every listener who listens to key presses, whether they are interested in that particular key or not, they still receive the event, and do the checks whether they want it on their own or not. I want to change that, I don't want the keyboard press event with the Key P for example to be sent to someone who looks for the Key K.

So here is my design, first we have an event dispatcher.

class Lina_ActionDispatcher
{
public:

    Lina_ActionDispatcher()
    {
    }

    std::list<Lina_ActionHandlerBase*> m_TestListeners;
};

Currently as you can see, I don't have any subscribe & unsubscribe method, which is going to be its own challenge because I want to handle the cases where the handler dies before the dispatcher or vice versa. Need recommendations on that as well. Anyways, I have a class called Lina_ActionHandlerBase, which is the base class for all actions. Since actions will be of type T, I basically wrote this base class as a wrapper. I hold raw pointer for them, firstly because I want to use polymorphism mainly, and also because these handlers will be created on the objects who wants to check for a particular action and will be passed to the dispatcher. So maybe a weak_ptr would be a better idea, so that I can check the expiration and clear the handler from the list when necessary. Looking for ideas here.

The dispatcher, would do this, when it wants to dispatch an action:

// it is the iterated element of the list, compare the action types (enumeration) call the control if they match
if(it->m_ActionType == action.m_ActionType)
it->Control(&action);

So, now let's take a look at the action classes.

// Base wrapper class for actions.
class Lina_ActionBase
{

public:
    Lina_ActionBase(){};
    ~Lina_ActionBase(){};
    Lina_ActionBase(ActionType at) : m_ActionType(at) {};
    virtual void* GetData() { return 0; }
    inline ActionType GetActionType() { return m_ActionType; }

private:
    Lina_ActionBase(const Lina_ActionBase& rhs) = delete;
    ActionType m_ActionType;

};

So this is the wrapper base class, that uses no templates, so I can hold a list in the dispatcher with pointers that point to an object of the base class. We have an ActionType, which basically the identifier of the event, like keyPress, physicsStepCalled, editorRendered etc. GetData is a virtual method, it returns void* because the type is undefined yet. The return type will be of type T in the derived, actual Action class, which is:

// Template class used for actions. 
template<typename T>
class Lina_Action : public Lina_ActionBase
{

public:
    Lina_Action() {}
    ~Lina_Action() { }
    inline void SetData(T t) { m_Value = t; }
    virtual void* GetData() { return &m_Value; }

private:
    T m_Value;

};

As you can see, now we can create any action with the value of type T. My Action structure is like this, we have an identifier of the action(m_ActionType) and the value of the action. For example a window operation, "windowClosed" can be id, while the value can be "textureWindow", or it can be an action of a key press, id would be "keyPressed", while value can be "KEY_F" you get the idea.

Now let's go to the real deal, the action handlers. As I have mentioned earlier, my dispatchers will hold a list of ActionHandlers, and these handlers can be any type T, so I have written a base wrapperclass for my handlers.

// Base wrapper class for action handlers.
class Lina_ActionHandlerBase
{
public:

    Lina_ActionHandlerBase() {}
    ~Lina_ActionHandlerBase() {}
    Lina_ActionHandlerBase(ActionType at) : m_ActionType(at) {};

    inline ActionType GetActionType() { return m_ActionType; }
    inline void SetActionType(ActionType t) { m_ActionType = t; }
    virtual void Control(Lina_ActionBase& action) { };
    virtual void Execute(Lina_ActionBase& action) {};


private:
    ActionType m_ActionType;
};

As i said earlier, what a dispatcher does is, when it receives an action, it iterates through the handlers, and asks them to control this event whether they want it or not. So my base class has a Control method, to be overriden in the derived handler classes.

I wanted to have handlers with diffent behaviour. For example, I could have a listener, who listens to key events, and doesn't care which key it is, it checks if the Action's identifier is "keyPress", if so, it just executes. Another handler may do the same, but also control which key was pressed. So here comes my concept of Condition. Some actions handlers may have conditions of type U, and what they will be doing is to check whether this type U is the same type with the action, then it would check whether the action has the same value as their condition value. So I have written another base class, which actually drives from the above ActionHandlerBase.

// Service class for actions handlers, determines the behaviour choices of actions.
template<typename T>
class Lina_ActionHandler_ConditionCheck : public Lina_ActionHandlerBase
{

public:

    Lina_ActionHandler_ConditionCheck() {};
    ~Lina_ActionHandler_ConditionCheck() {};
    Lina_ActionHandler_ConditionCheck(ActionType at) :
        Lina_ActionHandlerBase::Lina_ActionHandlerBase(at) {};


    inline void SetCondition(T t) { m_Condition = t; }
    inline T GetCondition() { return m_Condition; }

// Control block called by the dispatchers.
virtual void Control(Lina_ActionBase& action) override
{
    // Cast from action base class void* type to T*.
    T* dataPointer = static_cast<T*>(action.GetData()); 

    // Compare the type of the member attribute of type of the value value received from the action.
    // If types are the same, then check the values, if they are the same, execute
    if (CompareType(*dataPointer) && CompareValue(*dataPointer))
        Execute(action);
}

// Compares the data type U with member attribute of type T.
template<typename U>
bool CompareType(U u)
{
    if (std::is_same<U, T>::value)
        return true;

    return false;
}

// Compares the value of a variable typed U with the value of member attribute typed T.
template<typename U>
bool CompareValue(U u)
{
    return LinaEngine::Internal::comparison_traits<T>::equal(m_Condition, u);
}


private:
    T m_Condition;
};

So, as you can see, I have a condition of type T. When the Control method is called, it uses this condition. ( Whether the Control of this class will be called or not will be determined by another subclass, which is our actual action handler class, which i will explain below, so dont worry about it now ) What control does is, it extracts the void* typed value from the action base, casts it to a pointer of type T. What I am worried about here is the usage of static_cast, is there any better options or what do you guys think? Going on, then it uses is_same to check if they are the same type. As you can see in the if clause, if type check returns true, then we check for the value equality. I use comparison_trait struct to check the equality, which is this:

template <typename T> struct comparison_traits {
            static bool equal(const T& a, const T& b) {
                return a == b;
            }
        };

So if the action's value and the handlers condition is the same, our control succeeds and calls the execution method. What this gives is the flexibility of passing any kind of value to an action and action handler. I can pass ClassB, and as long as I have a comparison operator overloaded for ClassB, i can control how they are compared and everything would be fine.(in theory) So, if a control succeeds, then our execution is called. Let's look at the actual ActionHandler I will be using for handling stuff:

// Main derived class used for action handlers.
template<typename T = int>
class Lina_ActionHandler : public Lina_ActionHandler_ConditionCheck<T>
{
public:

    Lina_ActionHandler() {}
    ~Lina_ActionHandler() {}
    Lina_ActionHandler(ActionType at) :
        Lina_ActionHandler_ConditionCheck<T>::Lina_ActionHandler_ConditionCheck(at) {};

    inline void SetUseCondition(bool b) { m_UseCondition = b; }
    inline void SetUseBinding(bool b) { m_UseBinding = b; }
    inline void SetUseParamCallback(bool b) { m_UseParamCallback = b; }
    inline void SetUseNoParamCallback(bool b) { m_UseNoParamCallback = b; }


    inline bool GetUseBinding() { return m_UseBinding; }
    inline bool GetConditionCheck() { return m_UseCondition; }
    inline bool GetUseParamCallback() { return m_UseParamCallback; }
    inline bool GetUseNoParamCallback() { return m_UseNoParamCallback; }

    virtual void Control(Lina_ActionBase& action) override
    {

        // If condition check is used, call the control of the behaviour base class so it can compare it's member attribute T with the action's value.
        if (m_UseCondition)
            Lina_ActionHandler_ConditionCheck<T>::Control(action);
        else
            Execute(action);    // Execute the action if no check is provided.
    }

    virtual void Execute(Lina_ActionBase& action) override
    {
        // Call the callback with no parameters
        if (m_UseNoParamCallback)
            m_CallbackNoParam();

        // If we use parameterized callback or binding, we will extract the value from the action.
        // However, if we have not used condition, it means whe have not typed checked this value. So type check it first.
        if ((m_UseParamCallback || m_UseBinding))
        {
            // Cast from polymorphic action base class void* type to T*.
            T* typePointer = static_cast<T*>(action.GetData());

            if (!m_UseCondition)
            {

                // If the types do not match, simply exit.
                if (!Lina_ActionHandler_ConditionCheck<T>::CompareType(*typePointer))
                    return;
            }

            // Call the callback with parameters, cast and pass in the data from the action.
            if (m_UseParamCallback)
                m_CallbackParam(*typePointer);

            // Bind the value.
            if (m_UseBinding)
                *m_Binding = *typePointer;
        }

    }

    void SetParamCallback(std::function<void(T)> && cbp) { m_CallbackParam = cbp; }
    void SetNoParamCallback(std::function<void()>&& cb) { m_CallbackNoParam = cb; }
    void SetBinding(T* binding) { m_Binding = binding; }

private:
    bool m_UseParamCallback = false;
    bool m_UseNoParamCallback = false;
    bool m_UseBinding = false;
    bool m_UseCondition = false;
    T* m_Binding;
    std::function<void()> m_CallbackNoParam;
    std::function<void(T)> m_CallbackParam;
};

I wanted the action handlers to have different capabilities. They can check of a condition, or not. Completely seperate from conditions, their execution behaviours can be various. They can call a method, without any parameters upon execution, or they can call these methods with the parameter, received from the action (basically what the most of the event dispatching systems I have found does, they don't do any checks for anything else, they just send the action value to listeners.), or handlers can bind a particular value of type T, to the action value, for example a mouse motion event. You can pass a float to your handler, and the "mouseMotionX" action's value would be binded to your float, as soon as their types match and every object coupled are alive, you only have to bind once.

So remember, my dispatchers hold a list of ActionHandlerBase, but I will be creating objects of this ActionHandler class, and passing these objects to the list. So, since I use pointers in my dispatchers, the corresponding object methods, which will be ActionHandler class instances, would be called. Basically, whenever we call Control, it will be the Control of ActionHandler and it will execute from there according to the specified behaviour.

And to use the system, I would do:

    int toBind = 2;

    Lina_ActionDispatcher* disp = new Lina_ActionDispatcher;
    Lina_Action<float> action;
    action.SetData(22.5);

    auto f = []() {std::cout << "Non-Parameterized Callback Called" << std::endl; };
    auto f2 = [](float f) {std::cout << "Parameterized Callback Called, Value is : " << f << std::endl; };

    Lina_ActionHandler<float> b(ActionType1);

    b.SetUseCondition(true);
    b.SetUseNoParamCallback(true);
    b.SetUseParamCallback(true);

    b.SetCondition(22.5);
    b.SetNoParamCallback(f);
    b.SetParamCallback(f2);

    disp->m_TestListeners.push_back(&b);
    // TODO: Check action type before calling control.
    disp->m_TestListeners.front()->Control(action);

    std::cout << "Binded Variable Is: " << toBind;

So basically, this is my whole structure. I have couple of worries, about using *void in the ActionBase class, or using static_cast to cast it to type T in the condition checking and parameterized method calling or value binding. Also, if an handler doesnt use any condition, any parameterized callback or any binding, just a simple callback, then the condition, param. callback function pointer and binder pointer will be uninitialized. What should I do about them? Also I have worries about whether I have abused the boundaries of polymorphism, or used it in a silly way. I am looking for every single kind of recommendation, comment, advice from a single line to whole system architecture, and if I have left out anything to explain, please do state it. And also, you can read the full header file on Github, if the sharing is not allowed, please let me know.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.