Codementor Events

Design Patterns in Rust: The Command, a simple implementation of a versatile pattern

Published Jun 03, 2023

The command pattern is a behavioral design pattern. It is used by an _Invoker _to perform one action or a series of actions on objects, called Receivers. The Invoker is unaware of the nature of the receiving objects, it just knows about the command. That way we achieve a loose coupling between _Invoker _and Receiver.

It looks like this:
command.png

A short breakdown of this diagram:

  • The _Invoker _is the object want to execute a command or a series of commands on some unknown objects, that is, the _Invoker _might be unaware of them.
  • The _Command _interface. Many classes might implement this interface.
  • In this example there is just one class implementing the _Command _interface, CommandA, which works on ReceiverA. As you can see, the _Invoker _and the _Receiver _are not coupled, and the only communication between the two goes via the Command.

In an empty directory open your commandline or terminal and type:

cargo new rust_command
cd rust_command

Now open the main.rs file in your src/ directory.

We will start by defining the Command trait:

trait Command {
    fn execute(&self);
}

For the sake of simplicity, this has only method: execute() which executes the command.

We will start by defining a very simple command:

struct PrintCommand {
    message: String,
}
impl PrintCommand {
    fn new(message: String) -> PrintCommand {
        PrintCommand { message }
    }
}

impl Command for PrintCommand {
    fn execute(&self) {
        println!("{}", self.message);
    }
}

This code is quite self-explanatory: we have a constructor, new(), which sets the message. Next we implement the execute() method of the Command trait, which just prints out the message.

A slightly more complex example is this:

struct MultiplyCommand {
    a: i32,
    b: i32,
}

impl MultiplyCommand {
    fn new(a: i32, b: i32) -> MultiplyCommand {
        MultiplyCommand { a, b }
    }
}

impl Command for MultiplyCommand {
    fn execute(&self) {
        let result=self.a*self.b;
        println!("{} * {} = {}",self.a,self.b, result);
    }
}

A short breakdown:

  1. The MultiplyCommand struct has two i32 fields.
  2. We set these in the new() constructor.
  3. The execute() method multiplies these fields and prints an appropiate message.

We also need an object to invoke, or execute, these commands:

struct Invoker {
    commands: Vec<Box<dyn Command>>,
}

impl Invoker {
    fn new() -> Invoker {
        Invoker {
            commands: Vec::new(),
        }
    }

    fn add_command(&mut self, command: Box<dyn Command>) {
        self.commands.push(command);
    }

    fn execute_commands(&self) {
        for command in self.commands.iter() {
            command.execute();
        }
    }
}

Some explanation is needed:

  1. The Invoker struct has a list commands in the form of a Vec<Box<dyn Command>>. The dyn Command is needed because we need dynamic dispatching since command is a trait. Box is needed because we do not know the size of the objects added to the Vector.
  2. In the new() constructor, this vector is initialized.
  3. The add_command() method adds a new struct implementing the Command trait to the vector
  4. In the execute_commands() method, we iterate over the vector and execute each command.

Now we can test it:

fn main() {
    let mut invoker=Invoker::new();
    invoker.add_command(Box::new(PrintCommand::new("Hello World".to_string())));
    invoker.add_command(Box::new(MultiplyCommand::new(2,3)));
    invoker.execute_commands();
}

A line by line breakdown:

  1. We create an Invoker object. This has to be mutable since we are going to add commands to it.
  2. Next we add two commands to it, one PrintCommand, and one Multiplycommand. Notice how both these commands are Box-ed.
  3. Next we execute all these commands.

The Command pattern has many uses, such as in GUI programming, Mobile code, or parallel processing. As you can see here, the implementation can be quite simple. One of my next projects will be to see how we can use this pattern concurrently in a multithreaded environment.

What you can also see is that for example the building of an Undo-facility is quite easy. This is also an exercise I will do in one of my next blogs

Discover and read more posts from Iede Snoek
get started