🎓 A1Learner
🔍

Rust Command Pattern

The command pattern packages each action as a small value behind one trait. A command can run later, run again, or run backwards to undo itself.

One trait for every action

An action becomes a value by implementing this trait: run yourself, and undo yourself.

trait Command {
    fn execute(&self, doc: &mut String);
    fn undo(&self, doc: &mut String);
}

An action as a small struct

AddText carries the text it will add. Its undo removes exactly that many characters.

struct AddText(String);

impl Command for AddText {
    fn execute(&self, doc: &mut String) { doc.push_str(&self.0); }
    fn undo(&self, doc: &mut String) {
        for _ in 0..self.0.len() { doc.pop(); }
    }
}

Commands are data

Different command types share one list, as trait objects.

let mut queue: Vec<Box<dyn Command>> = Vec::new();
queue.push(Box::new(AddText(String::from("rust"))));
queue.push(Box::new(AddText(String::from("!"))));

Run the queue

Nothing happened when the commands were created. They run now, in list order.

let mut doc = String::new();
for cmd in &queue {
    cmd.execute(&mut doc);
}
println!("doc: {doc}");

Undo walks backwards

Reverse order matters: the last action done is the first one undone.

for cmd in queue.iter().rev() {
    cmd.undo(&mut doc);
}
println!("undone: '{doc}'");

Example

trait Command {
    fn execute(&self, doc: &mut String);
    fn undo(&self, doc: &mut String);
}

struct AddText(String);

impl Command for AddText {
    fn execute(&self, doc: &mut String) {
        doc.push_str(&self.0);
    }
    fn undo(&self, doc: &mut String) {
        for _ in 0..self.0.len() {
            doc.pop();
        }
    }
}

fn main() {
    let queue: Vec<Box<dyn Command>> = vec![
        Box::new(AddText(String::from("rust"))),
        Box::new(AddText(String::from("!"))),
    ];
    let mut doc = String::new();
    for cmd in &queue {
        cmd.execute(&mut doc);
    }
    println!("after run: {doc}");
    for cmd in queue.iter().rev() {
        cmd.undo(&mut doc);
    }
    println!("after undo: '{doc}'");
}

Key points

  • Command pattern: package each action as a value that knows how to run itself
  • Give every command one trait — an execute method, plus an undo method when the action can be reversed
  • Store commands as trait objects — Vec<Box<dyn Command>> — so one list can hold many different action types
  • Run the list in order when the moment comes.
  • Undo it in reverse order.
  • The last one done is the first one undone.
  • Reach for a named command type when the action carries data, or needs an undo

In this example, the queue holds two commands: add "rust", then add "!". Creating them changes nothing — the document is still empty. The first loop runs each command in order and the document becomes rust!. The second loop walks the queue in reverse and calls undo, so "!" is removed before "rust", and the document ends empty again.

Note

For a small one-off action, a stored closure — Box<dyn Fn> — is the same idea with less ceremony.

Try it Yourself

Edit the code, then press Run.

Running is simulated · Rust Playground

Exercise

Why does the command list store Box<dyn Command> values instead of one command struct type?