Command Design Pattern in Delphi
This session consists of the development of a small application to read and pretty-print XML and CSV files. Along the way, we explain and demonstrate the use of the following patterns: State, Interpreter, Visitor, Strategy, Command, Memento, and Facade.
Encapsulate a request as an object, thereby letting you parametrise clients with different requests, queue or log requests, and support undoable operations.
The Command pattern is implemented in Delphi (since Delphi 4) in actions. We won't use them, as I want to concentrate on the pattern. Actions are complex beasts, and this complexity might detract attention away from the points I want to make. There may well also be occasions where actions are not appropriate, so you may like to know how to roll your own Command structure.
We're going to use Command for the last of the above options, to implement an undo/redo list, and also to shield our user interface code from having to know anything about documents or the operations on them. After we've discussed how to do that, you should be able to see how you could add logging, queues, and so on.
In its simplest form, the Command pattern consists of an abstract base class with an Execute
method. Concrete subclasses are declared for each possible action, and the method implemented in each so as to carry out that action. Usually this requires a call to another class's interface. For instance, in our example, one command would be to open a file. After prompting for a file name, the open command will call a document object's OpenFile method. This second object is called the receiver. The object that asks the command to carry out a request is called the invoker. This is often something like a menu item or a button. We are going to do something a little different, though, as we'll see later.
The declaration of our command class (in DocumentCommands.pas) is:
TDocumentCommand = class(TObject)
private
FDocument : TDocument;
protected
procedure DoExecute; virtual; abstract;
procedure DoRollback; virtual;
// Used Self Encapsulate Field refactoring here. Now descendant commands
// can access the document, even if they are declared in other units
property Document : TDocument read FDocument write FDocument;
public
constructor Create(ADocument : TDocument);
procedure Execute;
procedure Rollback; // Reverse effect of Execute
end;
The implementation is:
constructor TDocumentCommand.Create(ADocument : TDocument);
begin
inherited Create;
FDocument := ADocument;
end;
procedure TDocumentCommand.DoRollback;
begin
end;
procedure TDocumentCommand.Execute;
begin
if Assigned(FDocument) then begin
DoExecute;
end;
end;
procedure TDocumentCommand.Rollback;
begin
if Assigned(FDocument) then begin
DoRollback;
end;
end;
Let's examine what's going on here. We have two public methods, one to perform a command, and another to reverse it. We need this last one because we want to support undo. Note that both these methods check to see if the document is available before calling another, protected, method. This is a very simple example of the Template pattern. We don't want programmers who are implementing new commands to have to remember to do that, so we will do it for them here, and they need to override the DoExecute
and DoRollback
methods instead.
The DoRollback
method implementation is empty because a command might not be able to be undone (we are only going to support this on pretty printing and search-and-replace), so we don't want to force subclasses to implement it by making the method abstract. However, we do want the command to be implemented, so the DoExecute
method is abstract to force it to be overridden.
The constructor gets the document passed as a parameter, and that will be used as the receiver in our example. For instance, the Open
command is implemented as follows:
procedure TOpenCommand.DoExecute;
var
FileName : string;
begin
if PromptForFileName(FileName,'XML files (*.xml)|*.xml|CSV files (*.csv)|*.csv') then begin
FDocument.OpenFile(FileName);
end;
end;
Here we call the Delphi function PromptForFileName
, and if the user doesn't cancel the operation, we call the document's OpenFile
method to load the text. For other commands, it gets a little more complex, and we start to see the advantages of separating the command logic out from the object that invokes it.
Other advantages are that it is easy to add new commands, because existing classes don't need to change. This makes it a lower risk modification to your code. You can also use the Composite pattern to make larger macro commands out of several other commands.
Although our command implementations all make use of a receiver to ultimately carry out the operation, that's not always necessary. The GoF example is that of a command to launch another application. The command may well have enough information to do that itself (by calling ShellExecute
, say), without having to delegate the task to another object. It may also be necessary sometimes for the command to find its receiver dynamically. If our example application used MDI, then we might need to go and find the current document, for instance.