Combining Command and Visitor design patterns

59 Views Asked by At

Designing the architecture of a personal project, I've come up with the idea of using the combination of these two patterns to solve an architectural issue. In an MVC context, I need to implement various types of actions triggered by the UI. I want to abstract the controller from the execution logic, with the model being responsible for this. The issue is that I have different types of commands, and depending on the type of command, it should be routed through different logical paths of the program. Therefore, I need not only to delegate the action to be performed to the command itself but also to route it.

This is an example of the design I want to implement:

class ICommand {
public:
    virtual ~ICommand() {}
    virtual void execute() = 0;
    virtual void accept(CommandVisitor& visitor) = 0;
};

class CommandVisitor {
public:
    void visit(SaveFileCommand& command);
    void visit(SaveConfiguration& command);

    void visit(SaveFileCommand& command) {
        // logic
    }

    void visit(SaveConfiguration& command) {
        // logic
    }
};

class SaveFileCommand : public ICommand {
public:
    void execute() override {
        // logic
    }
    void accept(CommandVisitor& visitor) override {
        visitor.visit(*this);
    }
};

class SaveConfiguration : public ICommand {
public:
    void execute() override {
        // logic
    }
    void accept(CommandVisitor& visitor) override {
        visitor.visit(*this);
    }
};

My concern is creating a design that's too complex, and I'm not sure if it's the best approach for my problem. What alternatives do I have besides this?

EDIT: Adding more context about the domain

My application consists of simulating one end of a communication.

In an MVC context, I need to communicate the model and the view bidirectionally through the controller. The controller will do nothing but forward the messages it receives from either end, making it possible for both ends (Model and View) not to know each other. The view will generate various types of messages to send to the model for processing, and the model will generate various messages to send to the view for display. The situation is that I need these messages to take different logical paths within the program once they reach the view or the model. Some examples:

  • In view 1, a configuration message is generated and sent to the model to open the TCP port.
  • In view 2, a protocol message is generated and sent to the model to be sent via TCP.
  • In view 3, a protocol message is generated and sent to the model to be sent via UDP.
  • The model receives a message via TCP and it is displayed in view 2.
  • The model receives a message via UDP and it is displayed in view 3.

Therefore, the logical sequence that any message should follow is:

  • It is generated in view/model.
  • It is sent to the controller and gets forwarded.
  • It reaches the view/model and depending on the type, it is directed to one logical path or another.

In other words, I have a series of different "hierarchical" messages that need to be dispatched according to their type.

1

There are 1 best solutions below

1
rwdougla On

It's a good idea to scratch out some example commands to guide your architectural direction. Based purely on "SaveFileCommand" and "SaveConfigurationCommand", I find myself wondering whether you intend all commands to be non-mutating like these, or whether further commands are going to include things which mutate your internal state? This is a critical decision point, because scoping out a more representative set of commands will drastically help inform your architecture. for example:

  1. If you have commands to modify UI state, you may wish to add those commands onto an "undo" stack, and may thus wish to have a second command-handler separate from CommandVisitor, which manages a stack of commands.
  2. If you have commands which need to reach out to external processes, you may want to have a command-handler which offloads asynchronous calls to a thread or thread pool to manage such operations asynchronously. The same could even be said for SaveFileCommand and SaveConfigurationCommand, which may grow in processing scope to need justify offloading heavy I/O work.
  3. If the commands include types of numerous highly-frequent operations, like modifying elements of a massive in-memory data store, you might favor a command-handler which wishes to skip any virtual calls or dynamic memory for performance concerns.
  4. You may even find yourself asking whether there commands which may impact the processing behavior of the CommandVisitor, itself: will the CommandVisitor hold state of its own which may be modified? What kinds of commands might define such operations?

My strong recommendation would be to continue writing out the commands you expect to use, and as you go with that, write out the CommandVisitor. Do this by leveraging unit-tests to see what the experience is like in writing new commands or extending the CommandVisitor, and whether you find friction that might justify breaking up the CommandVisitor into numerous "command handlers".

If you're interested in more reading in the area, you might checkout Qt's QUndoCommand architecture for inspiration on managing commands in a GUI, or check out any of the number of messaging bus systems like Kafka or Aeron for managing commands flowing between systems.

If you're looking for more C++-specific structures to help define the commands, I like Jarod42's suggestion to consider std::variant, as it can be a nice way to model the group of commands a command-handler would support, without requiring them to share an object hierarchy. I've found good mileage from that in similar situations, myself.