Command Pattern
Command is behavioral design pattern that converts requests or simple operations into objects.
The conversion allows deferred or remote execution of commands, storing command history, etc.
Golang Implementation
Example: TV Remote
Let’s look at the Command pattern with the case of a TV. A TV can be turned ON by either:
- ON Button on the remote;
- ON Button on the actual TV.
We can start by implementing the ON command object with the TV as a receiver. When the execution method is called on this command, it, in turn, calls the TV.on function. The last part is defining an invoker. We’ll actually have two invokers: the remote and the TV itself. Both will embed the ON command object.
Notice how we have wrapped the same request into multiple invokers. The same way we can do with other commands. The benefit of creating a separate command object is that we decouple the UI logic from underlying business logic. There’s no need to develop different handlers for each of the invokers. The command object contains all the information it needs to execute. Hence it can also be used for delayed execution.
command.go
package main
type Command interface {
execute()
}
onCommand.go
package main
type OnCommand struct {
device Device
}
func (c *OnCommand) execute() {
c.device.on()
}
offCommand.go
package main
type OffCommand struct {
device Device
}
func (c *OffCommand) execute() {
c.device.off()
}
device.go
package main
type Device interface {
on()
off()
}
tv.go
package main
import "fmt"
type Tv struct {
isRunning bool
}
func (t *Tv) on() {
t.isRunning = true
fmt.Println("Turning tv on")
}
func (t *Tv) off() {
t.isRunning = false
fmt.Println("Turning tv off")
}
tvButton.go
package main
type TvButton struct {
command Command
}
func (b *TvButton) press() {
b.command.execute()
}
remoteButton.go
package main
type RemoteButton struct {
command Command
}
func (b *RemoteButton) press() {
b.command.execute()
}
main.go
package main
func main() {
tv := &Tv{}
onCommand := &OnCommand{
device: tv,
}
offCommand := &OffCommand{
device: tv,
}
onTvButton := &TvButton{
command: onCommand,
}
onTvButton.press()
offTvButton := &TvButton{
command: offCommand,
}
offTvButton.press()
onRemoteButton := &RemoteButton{
command: onCommand,
}
onRemoteButton.press()
offRemoteButton := &RemoteButton{
command: offCommand,
}
offRemoteButton.press()
// Beautiful part of this pattern:
// use remote to turn on and use tv button to turn off
onRemoteButton.press()
offTvButton.press()
}
// Turning tv on
// Turning tv off
// Turning tv on
// Turning tv off
// Turning tv on
// Turning tv off
Rust Implementation
In Rust, a command instance should NOT hold a permanent reference to global context, instead the latter should be passed from top to down as a mutable parameter of the “execute” method:
fn execute(&mut self, app: &mut cursive::Cursive) -> bool;
Example: Text Editor
Key points:
- Each button runs a separate command.
- Because a command is represented as an object, it can be pushed into a history array in order to be undone later.
- TUI is created with cursive crate.
command.rs
mod copy;
mod cut;
mod paste;
pub use copy::CopyCommand;
pub use cut::CutCommand;
pub use paste::PasteCommand;
/// Declares a method for executing (and undoing) a command.
///
/// Each command receives an application context to access
/// visual components (e.g. edit view) and a clipboard.
pub trait Command {
fn execute(&mut self, app: &mut cursive::Cursive) -> bool;
fn undo(&mut self, app: &mut cursive::Cursive);
}
command/copy.rs
use cursive::{views::EditView, Cursive};
use super::Command;
use crate::AppContext;
#[derive(Default)]
pub struct CopyCommand;
impl Command for CopyCommand {
fn execute(&mut self, app: &mut Cursive) -> bool {
let editor = app.find_name::<EditView>("Editor").unwrap();
let mut context = app.take_user_data::<AppContext>().unwrap();
context.clipboard = editor.get_content().to_string();
app.set_user_data(context);
false
}
fn undo(&mut self, _: &mut Cursive) {}
}
command/cut.rs
use cursive::{views::EditView, Cursive};
use super::Command;
use crate::AppContext;
#[derive(Default)]
pub struct CutCommand {
backup: String,
}
impl Command for CutCommand {
fn execute(&mut self, app: &mut Cursive) -> bool {
let mut editor = app.find_name::<EditView>("Editor").unwrap();
app.with_user_data(|context: &mut AppContext| {
self.backup = editor.get_content().to_string();
context.clipboard = self.backup.clone();
editor.set_content("".to_string());
});
true
}
fn undo(&mut self, app: &mut Cursive) {
let mut editor = app.find_name::<EditView>("Editor").unwrap();
editor.set_content(&self.backup);
}
}
command/paste.rs
use cursive::{views::EditView, Cursive};
use super::Command;
use crate::AppContext;
#[derive(Default)]
pub struct PasteCommand {
backup: String,
}
impl Command for PasteCommand {
fn execute(&mut self, app: &mut Cursive) -> bool {
let mut editor = app.find_name::<EditView>("Editor").unwrap();
app.with_user_data(|context: &mut AppContext| {
self.backup = editor.get_content().to_string();
editor.set_content(context.clipboard.clone());
});
true
}
fn undo(&mut self, app: &mut Cursive) {
let mut editor = app.find_name::<EditView>("Editor").unwrap();
editor.set_content(&self.backup);
}
}
main.rs
mod command;
use cursive::{
traits::Nameable,
views::{Dialog, EditView},
Cursive,
};
use command::{Command, CopyCommand, CutCommand, PasteCommand};
/// An application context to be passed into visual component callbacks.
/// It contains a clipboard and a history of commands to be undone.
#[derive(Default)]
struct AppContext {
clipboard: String,
history: Vec<Box<dyn Command>>,
}
fn main() {
let mut app = cursive::default();
app.set_user_data(AppContext::default());
app.add_layer(
Dialog::around(EditView::default().with_name("Editor"))
.title("Type and use buttons")
.button("Copy", |s| execute(s, CopyCommand))
.button("Cut", |s| execute(s, CutCommand::default()))
.button("Paste", |s| execute(s, PasteCommand::default()))
.button("Undo", undo)
.button("Quit", |s| s.quit()),
);
app.run();
}
/// Executes a command and then pushes it to a history array.
fn execute(app: &mut Cursive, mut command: impl Command + 'static) {
if command.execute(app) {
app.with_user_data(|context: &mut AppContext| {
context.history.push(Box::new(command));
});
}
}
/// Pops the last command and executes an undo action.
fn undo(app: &mut Cursive) {
let mut context = app.take_user_data::<AppContext>().unwrap();
if let Some(mut command) = context.history.pop() {
command.undo(app)
}
app.set_user_data(context);
}