Command Pattern
(from Design Patterns and PLoP'95)
Synopsis
Encapsulate a request as an object, thereby letting you parameterize
clients with different requests, queue or log requests, and support
undoable operations.
Context
You want to model the time evolution of a program:
- What needs to be done, e.g. queued requests, alarms, conditions for
action.
- What is being done, e.g. which parts of a composite or distributed
action have been completed.
- What has been done, e.g. a log of undoable operations.
In other words, you want a kind of Reflection where what is being described
is work flow, not just instantaneous data.
Forces
-
The number and implementation of program operations can vary.
-
Operations should be undoable, e.g. if the request was mistaken or a
failure occurred in the middle of a composite action.
-
Additional functionality related to an operation, such as logging, should be
decoupled from the operation itself.
Solution
Explicitly represent units of work as Command objects. Units of work
depend on the application and may range from reading and writing individual
variables to reformatting a text document. The interface of a Command
object can be as simple as a DoIt()
method. Extra methods can
be used to support undo and redo. Commands can be persistent and globally
accessible, just like normal objects.
Commands are parameterized by the variable to be written, word to delete,
place to move a figure to, etc. These parameters should be abstract, to
avoid embedded references which would hinder logging and communicating a
Command. For example, variables should be referred to by name, not
pointer, and words should be referred to by position in the text.
For the Command to perform its own undo, it needs to store information
destroyed by the operation. For example, the old value of the variable,
the contents of the word being deleted, and the old position of the figure.
The UndoIt()
method can use this to back out of the operation.
For safety, the Command could also store whether it has been done or not.
Commands are sent to a Command Processor which may queue, prioritize,
and distribute them among machines or threads. The Command Processor can
take care of logging the Commands for future undo. It can also schedule
a Command to occur at a particular time or under specific conditions.
When all Commands are logged, storing the program state is redundant,
because the program state at any moment in time can be reconstructed by
replaying the log up to that point. Thus program state is essentially a
cache of the computation stored the log. This offers a simple way to
implement undo: blow away the current state and reconstruct the state at a
previous time. To speed up this operation, the state can be periodically
written, or checkpointed, to the log. State can then be
reconstructed by starting with the last checkpoint and replaying the
Commands since then.
Consequences
-
Since Commands are encapsulated, their number and implementations can vary.
-
Commands have independent lifetimes. Thus they can be queued, stored, or
transmitted.
-
Commands can be logged to support undo and redo, either because of user
error or system crash. Commands can encapsulate their own undo procedure,
or the log can be replayed to simulate undo.
-
Commands can be assembled into macros and scripts, which are also
Commands. This is one way to represent atomic transactions. The composite
Command registers its start and end in the log. If an error or failure
occurs in the middle, all Commands are undone up to the start marker.
-
A Command can play the role of a Strategy or callback, decoupling the
issuer of the Command from the Command's implementation. For example, a
button can initiate an arbitrary operation depending on what its Command
Strategy was. The same Command can come from several different sources.
Implementation
-
Programs do many things, so there may be an excessive number of different
Commands. This can be avoided by defining all operations in terms of a
small number of kernel Commands, a so-called bottleneck design. The
graphical interface, command-line interface, and network interface all
create the same kernel Commands. Bottlenecks are commonly used in
programming languages, where most syntax is defined in terms of a smaller,
kernel language (the extraneous syntax being called "sugar"). Frameworks
like ET++ use bottlenecks to minimize the number of methods that need to be
overridden in a subclass. Some OpenDoc components create a bottleneck by
sending OSA events to themselves rather than directly invoking methods.
This makes it easy to log and override the actions of the component.
-
The undo operation can be a direct message to the Command Processor or can
be a Command of its own. In the former case, undos do not enjoy the
advantages of regular Commands, and have no audit trail. In the latter
case, only a single-level of undo is possible, because a second undo will
actually be a redo.
-
The above problems with undo can be avoided by using anti-Commands,
which are Commands that happen to undo previous Commands. For example,
x--
is the anti-Command to x++
. A Command should
be able to produce its own anti-Command. Anti-Commands allow non-sequential
undo, i.e. undoing a Command which wasn't the most recently done.
-
Commands can be passed along a Chain of Responsibility for consumption.
-
As Ralph Johnson points out in Transactions and
Accounts (PLoP'95),
sometimes the implementation of a Command needs to be changed or an
operation needs to be associated with a different Command. Such changes
can also be modeled as Commands, allowing them to be recorded and undone.
See Design
Patterns for sample code.
Known Uses
-
ET++ uses the Command pattern to dispatch user interface events.
Typical Commands are "move from here to there", "resize from this to that",
and "delete the tenth word of the document". Commands support their own
UndoIt()
method.
Since event handlers do nothing but queue Commands, control immediately
returns to the event loop, where Commands are executed when the system is
idle, making the interface seem more responsive.
-
OpenDoc uses the Command pattern to encapsulate OSA
events. Events can be composed in complex ways by script objects. Command
parameters are abstract, e.g. "the first movie in the second paragraph",
allowing Commands to be easily stored and transmitted, but requiring
sophisticated name lookup.
-
Transaction processing systems uses the Command pattern to log primitive
operations like reading and writing memory locations. In case of system
failure, the log is used to reconstruct the program state from the last
checkpoint. Atomic transactions which had not completed simply return
failure and do not have their component Commands redone.
-
Caterpillar's fate is a
design method emphasizing the use of Command objects, called work
packets, for structuring a design.
- More uses are described in this student project.
Thomas Minka
Last modified: Fri Sep 02 15:19:03 GMT 2005