Menu
Coddy logo textTech

Command Pattern

Part of the Object Oriented Programming section of Coddy's Dart journey — lesson 94 of 110.

The Command pattern encapsulates a request as an object, allowing you to parameterize clients with different requests, queue operations, or support undo functionality. Instead of directly calling a method, you wrap the action in a command object that can be stored, passed around, or executed later.

The pattern involves three key components: a Command interface with an execute method, Concrete Commands that implement specific actions, and a Receiver that performs the actual work. An optional Invoker triggers commands without knowing what they do:

abstract class Command {
  void execute();
}

class Light {
  void turnOn() => print('Light is ON');
  void turnOff() => print('Light is OFF');
}

class TurnOnCommand implements Command {
  final Light light;
  TurnOnCommand(this.light);

  @override
  void execute() => light.turnOn();
}

class TurnOffCommand implements Command {
  final Light light;
  TurnOffCommand(this.light);

  @override
  void execute() => light.turnOff();
}

class RemoteControl {
  Command? _command;

  void setCommand(Command command) {
    _command = command;
  }

  void pressButton() {
    _command?.execute();
  }
}

void main() {
  var light = Light();
  var remote = RemoteControl();

  remote.setCommand(TurnOnCommand(light));
  remote.pressButton();  // Light is ON

  remote.setCommand(TurnOffCommand(light));
  remote.pressButton();  // Light is OFF
}

The RemoteControl doesn't know anything about lights - it just executes whatever command it's given. This decoupling makes it easy to add new commands without modifying the invoker. The Command pattern is ideal for implementing undo/redo systems, task queues, or macro recording where you need to treat actions as first-class objects.

challenge icon

Challenge

Easy

Let's build a text editor command system using the Command pattern! You'll create a system that encapsulates text operations as command objects, allowing an editor to execute different actions without knowing the details of each operation. This is a classic use case for the Command pattern - think of it like an undo/redo system where each action is stored as an object.

You'll organize your code into two files:

  • commands.dart: Create your command system here. Start with an abstract class Command that declares an execute() method. Then create a TextDocument class (the receiver) that holds a String content field (initially empty) and has three methods:
    • append(String text) - adds text to the end of content
    • clear() - sets content to an empty string
    • show() - prints Document: [content]
    Now implement three concrete commands that work with a TextDocument:
    • AppendCommand - takes a document and text in its constructor, and appends the text when executed
    • ClearCommand - takes a document in its constructor, and clears it when executed
    • ShowCommand - takes a document in its constructor, and shows its content when executed
    Finally, create a TextEditor class (the invoker) that stores a list of commands. It should have an addCommand(Command command) method to queue commands, and a runAll() method that executes all queued commands in order.
  • main.dart: Import your commands file and demonstrate the Command pattern. Create a TextDocument and a TextEditor. Queue up the following commands in order: append Hello, append World, show the document, append !, show again, clear the document, and show one final time. Then run all the queued commands.

Notice how the TextEditor doesn't know anything about appending, clearing, or showing - it just executes whatever commands it's given. This decoupling is the essence of the Command pattern!

Expected output:

Document: Hello World
Document: Hello World!
Document: 

Cheat sheet

The Command pattern encapsulates a request as an object, allowing you to parameterize clients with different requests, queue operations, or support undo functionality.

The pattern involves three key components:

  • Command interface with an execute method
  • Concrete Commands that implement specific actions
  • Receiver that performs the actual work
  • Invoker (optional) that triggers commands without knowing what they do

Basic structure:

abstract class Command {
  void execute();
}

class Receiver {
  void action() => print('Action performed');
}

class ConcreteCommand implements Command {
  final Receiver receiver;
  ConcreteCommand(this.receiver);

  @override
  void execute() => receiver.action();
}

class Invoker {
  Command? _command;

  void setCommand(Command command) {
    _command = command;
  }

  void trigger() {
    _command?.execute();
  }
}

Example with a light control system:

class Light {
  void turnOn() => print('Light is ON');
  void turnOff() => print('Light is OFF');
}

class TurnOnCommand implements Command {
  final Light light;
  TurnOnCommand(this.light);

  @override
  void execute() => light.turnOn();
}

class RemoteControl {
  Command? _command;

  void setCommand(Command command) {
    _command = command;
  }

  void pressButton() {
    _command?.execute();
  }
}

void main() {
  var light = Light();
  var remote = RemoteControl();

  remote.setCommand(TurnOnCommand(light));
  remote.pressButton();  // Light is ON
}

The invoker doesn't know anything about the receiver - it just executes whatever command it's given. This decoupling makes it easy to add new commands without modifying the invoker.

Try it yourself

import 'commands.dart';

void main() {
  // TODO: Create a TextDocument instance

  // TODO: Create a TextEditor instance

  // TODO: Queue the following commands in order:
  // 1. Append "Hello"
  // 2. Append " World"
  // 3. Show the document
  // 4. Append "!"
  // 5. Show the document again
  // 6. Clear the document
  // 7. Show the document one final time

  // TODO: Run all queued commands
}
quiz iconTest yourself

This lesson includes a short quiz. Start the lesson to answer it and track your progress.

All lessons in Object Oriented Programming