lundi 13 septembre 2021

How can I fix and/or re-design this event class system to not require exclusively public data members?

I have an Event system in which a Manager class contains a queue of Events. The Event class is currently an abstract base class from which all specific events are derived. The idea is to allow the Manager to process() specific events that are specialized classes constructed with their own, unique parameters elsewhere in the code where the context of creation informs which event to create and how (sometimes using an Abstract Factory pattern). These Events are consumed asynchronously by Manager in a loop that continuously empties the queue, processing each event, and then destroying it as it is popped.

I designed this system to alleviate Manager from doing all of its own data manipulation. Instead, other parts of the code can queue an Event object, and that object can perform a task via its process() member, with the Manager not needing to know about it in detail.

Without an encapsulated Event system, I would be making endless additions to Manager's members: one for each task, and calling those tasks in case statements somewhere, which is terrible. I thought it would be better to use the Event system I have in the example here. This leaves Manager unchanged while various derived, specific events are defined elsewhere. This minimal example works fine, but the problem is that all derived Events need access to Manager's private data for each one's process() to have full capability as intended. If friend inheritance was allowed, simply making Event a friend class of Manager would solve the problem. Since it's not, the only way for this to work is to make all of Manager's data public (or, technically, add every new derived Event class I make as a friend to Manager).

This works, but feels wrong, and makes me think this is not the correct design. To be clear, the Manager contains a good deal of centralized information (necessary for thread sync, etc) that is more extensive than the Example would indicate. It manages network connections that will spawn a variety of different, often arbitrary events. I'd like to elegantly react to such an environment without inflating Manager with endless additional member functions every time I want to create a new type of event.

Is there a better way to achieve this code separation pattern and still retain the freedom I want for the separated code? Should I just publicize all of Manager's data after all?

The minimal Example (includes pseudocode):

class Event;

class Manager
{
    public:
        Manager() {}
    
    // Event queue insertion and processing functions omitted    
    
    // private:  // Commented out on purpose to allow compilation
        int dataInt;
        double dataReal;
        std::string dataStr;
        std::queue<Event *> events;
};

// Abstract Event base class with process() member
class Event
{
    public:
      Event(Manager * m) : manager(m)
      { 
        process = std::bind(&Event::processKernel, this);
      }
      
      // Process the event 
      std::function<void(void)> process;
      
    protected:
      
      // Actual processing code: derived classes must define this function
      virtual void processKernel() = 0;
    
    private:
      Manager * m;
   
};

// One example of a specialized (derived) Event
class SpecificEvent: public Event
{
    public:
        SpecificEvent(Manager * m, int p) : Event(m), param(p) { } 
        
        void processKernel() 
        {
            // Intention: modify parent manager data
            manager->dataInt = param;
        }
        
    private:
        int param;
};

// Another specialized (derived) Event
class OtherEvent: public Event
{
    public:
        OtherEvent(Manager * m, double p) : Event(m), param(p) { } 
        
        void processKernel() 
        {
            // Intention: modify parent manager data
            manager->dataReal = param;
        }
        
    private:
        double param;
};

// Example usage: could be inside a Manager member, or anywhere else
int main()
{
    Manager manager;
    
    // Make a SpecificEvent, pass it the manager, and its own specific parameter(s)
    SpecificEvent e(&manager, 10);
    
    //<Not shown> Add to manager's queue
    // Manager processes this event at some point later with Event::process()
}

Aucun commentaire:

Enregistrer un commentaire