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
EasyLet'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 classCommandthat declares anexecute()method. Then create aTextDocumentclass (the receiver) that holds aString contentfield (initially empty) and has three methods:append(String text)- adds text to the end of contentclear()- sets content to an empty stringshow()- printsDocument: [content]
TextDocument:AppendCommand- takes a document and text in its constructor, and appends the text when executedClearCommand- takes a document in its constructor, and clears it when executedShowCommand- takes a document in its constructor, and shows its content when executed
TextEditorclass (the invoker) that stores a list of commands. It should have anaddCommand(Command command)method to queue commands, and arunAll()method that executes all queued commands in order.main.dart: Import your commands file and demonstrate the Command pattern. Create aTextDocumentand aTextEditor. Queue up the following commands in order: appendHello, appendWorld, 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
}
This lesson includes a short quiz. Start the lesson to answer it and track your progress.
All lessons in Object Oriented Programming
1Fundamentals of OOP
External FilesLibraries & ImportsIntroduction to OOPClasses vs ObjectsThe this KeywordMethodsInstance VariablesConstructor BasicsRecap - Simple Calculator4Null Safety
Intro to Null SafetyNullable vs Non-NullableThe ? and ! OperatorsLate Keyword & Null SafetyNull-Aware OperatorsNull Safety in ClassesRecap - User Profile System7Abstract Classes & Interfaces
Abstract ClassesAbstract MethodsInterfaces in DartImplicit InterfacesImplementing vs ExtendingMultiple InterfacesRecap - Shape Calculator10Collections & Generics
List, Set, Map OverviewType-Safe CollectionsGeneric ClassesGeneric MethodsGeneric ConstraintsIterable & IteratorRecap - Generic Storage13Advanced OOP Concepts
Composition vs InheritanceExtension MethodsCallable ClassesSealed Classes (Dart 3)Records (Dart 3)Patterns & Matching (3.0)Enums with Methods16Project: Library Management
Project OverviewBook and User Classes2Constructors in Dart
Default ConstructorNamed ConstructorsInitializer ListsConstant ConstructorsFactory ConstructorsRedirecting ConstructorsRecap - Shape Builder5Encapsulation
Public vs Private MembersThe _ Prefix ConventionLibrary-Level PrivacyGetters & Setters DepthInformation HidingRecap - Student Records8Mixins
Introduction to MixinsCreating MixinsUsing Multiple Mixinson Keyword in MixinsMixin vs InheritanceMixin vs InterfaceRecap - Animal System11Special Methods
toString() OverridehashCode & == OverrideComparable Interfacecall() MethodnoSuchMethod OverrideRecap - Custom Collection14Design Patterns Part 1
Intro to Design PatternsSingleton PatternFactory PatternObserver PatternStrategy Pattern3Class Properties
Instance vs Static MembersFinal & Const FieldsLate VariablesStatic Methods & FieldsGetters and SettersRecap - Bank Account Manager6Inheritance
Basic InheritanceThe super KeywordMethod OverridingThe @override AnnotationThe final Class KeywordConstructors & InheritanceRecap - Employee Hierarchy9Polymorphism
Polymorphism BasicsPolymorphism via InterfacesRuntime Type CheckingThe is & as OperatorsCovariant KeywordRecap - Payment Processor12Async OOP
Futures & async/awaitStreams BasicsStream ControllersAsync ConstructorsAsync in Class MethodsRecap - Data Fetcher15Design Patterns Part 2
Command PatternAdapter PatternDecorator PatternTemplate Method PatternState PatternComposite PatternRepository Pattern