command-delphi
command-delphi copied to clipboard
Command pattern for Delphi (IComand & TComand)
Command Pattern for Delphi
Overview
Simplified version of the GoF Command Pattern, created for the purposes of modernization of VCL projects. Also added action factory to this project, which is wrapping a command into VCL action.
The Command Pattern

Implementation
The project contains two versions of the pattern implementation:
- classic Gang of Four
ICommandinterface - VCL
TCommandclass based on TComponent
Modernization process
The TCommand component was created to help the modernization of the legacy VCL code. It assists the extraction of tangled code, which after securing it with unit tests, can be refactored into cleaner and cheaper to maintain object-oriented code.
TCommand component is a transition object that should be refactored after clearing extracted code and after removing UI dependencies

TCommand component
The easiest way to use the TCommand component is to create a new class, paste long method into Execute method and add all dependencies as published properties. See sample bellow.
Diagram of TCommand usage in the VCL application:

Creating / implementing new command
Developer to build new command needs to define new class derived from TCommand (unit: Pattern.Command.pas) and implements a protected method DoExecute, which contains a main command logic.
Developer can implement a method DoGuard also, which is called before DoExecute and allow to verify all mandatory injections (injection system is explained bellow). Usually all injections are checked with Assert call.
Sample command without injection (empty guard):
type
TDiceRollCommand = class (TCommand)
protected
procedure DoExecute; override;
end;
procedure TDiceRollCommand.DoExecute;
begin
ShowMessage('Dice roll: '+RandomRange(1,6).ToString);
end;
To execute command you should create object and call Execute public method, which call DoGuard and then DoExecute:
cmd := TDiceRollCommand.Ceate(Self);
cmd.Execute;
TCommand injection system
TCommand component has built in automated injection system based on classic RTTI mechanism used by IDE Form Designer (Object Inspector). Properties exposed to be injectable have to be defined in published section of the component (command). All component based classes have switched on run-time type information generation during compilation process (compiler option {$TYPEINFO ON}). Thanks of that during creation of new command all dependencies can be easily provided and assigned to published properties automatically. More information about classic RTTI engine can be find in Delphi documentation: Run-Time Type Information
Sample command with two dependencies (one required and one optional):
type
TDiceRollCommand = class (TCommand)
const
RollCount = 100;
private
fOutput: TStrings;
fProgressBar: TProgressBar;
procedure ShowProgress(aRoll: integer);
protected
procedure DoGuard; override;
procedure DoExecute; override;
published
property OutputRolls: TStrings read fOutput
write fOutput;
property ProgressBar: TProgressBar read fProgressBar
write fProgressBar;
end;
procedure TDiceRollCommand.DoGuard;
begin
System.Assert(fOutput<>nil);
end;
procedure TDiceRollCommand.ShowProgress(aRoll: integer);
begin
if Assigned(fProgressBar) then begin
if aRoll=0 then
fProgressBar.Max := RollCount;
fProgressBar.Position := aRoll;
end;
end
procedure TDiceRollCommand.DoExecute;
begin
ShowProgress(0);
for var i := 0 to RollCount-1 do
begin
fOutput.Add(RandomRange(1,7).ToString);
ShowProgress(i+1);
end;
end;
Available published properties of TCommand are matched against types of parameters passed in parameters (open array). Following rules are used by matching algorithm:
- The same object types are matched
- If there is two or more object of the same class passed and more matching properties then parameter are assigned to properties according to order first with first, second with second, etc.
- More specific object passed as parameter is matching to more general object in properties list
- Numeric integer parameters are assigned to numeric properties
- Strings to strings
- Supported are also decimals, enumerable and boolean types.
Warning! Injected object are accessed by address in memory (pointer), thanks of that any changes made to object are visible inside and outside of the TCommand. Simple types and strings are accessed via value and properties have to updated manually to be updated.
Sample code injecting objects to properties of TDiceRollCommand:
cmd := TDiceRollCommand.Create(Self)
.Inject([Memo1.Lines,ProgressBar1]);
Most popular and usually advised method of injecting dependencies is a constructor injection. This solution introduced here (TCommand pattern) is more component based approach. This pattern is more like a transition stage which allow quickly extract and execute important parts of big application. Final target point in that process is the best architectural solution, means injection through the constructor and use interfaces instead of objects.
TCommand execution
- Instant (ad-hoc) command execution
TCommand.AdhocExecute<T>- executes a command (creates a command, injects dependencies executes it and removes)
- Full command construction and execution
- Create command with standard (component) constructor
- Call method
Inject - Execute command with
Execute
- Build command invoker
TCommandActionwhich executes the command when the action is invokedTCommandActionclass is classic VCL action- This class has special methods to allow rapid construction and initialization
Asynchronous Command
Business logic, extracted into the command, can be easily converted into asynchronous command, processed in a separate background thread. Replacing TCommand class with TAsyncCommand is first steep in such transformation:
uses
Pattern.AsyncCommand;
type
TAsyncDiceRollCommand = class (TAsyncCommand)
...
end;
Although the change is very simple, but in general, multi-threaded processing is a much more serious subject and requires deeper knowledge of this area. In this example (TDiceRollCommand) two topics are problematic:
- Access to UI control
fProgressBar: TProgressBar - Access to shared memory
fOutputRolls: TStrings
You can easily deal with them, but this requires more general multithread processing knowledge. More info you can find in dedicated documentation: Asynchronous Command
TCommandAction - VCL command invoker
TCommandAction is a wrapper class based on TAction and is able to execute commands based on TCommand class. Developer, when building VCL application, can easily bind this action to many controls (visual components which are driven by actions or are action-aware). For example TCheckBox has Action property which is executed when used is changing checkbox state (checked). Actions have some other advantages like build in notification system, precisely two such engines: one for updating visual state and another, more internal, for notifying about creation of new and deletion of existing components. Both engines are too complex to be described in this section, more information can be found in the Delphi online documentation.
Looking form architectural perspective TCommandAction can be used as an Invoker object and after migration can be replaced by more elastic custom solution.
Sample construction on TCommandAction invoker:
Button1.Action := TCommandAction.Create(Button1)
.WithCaption('Run sample command')
.WithCommand(TSampleCommand.Create(Button1))
.WithInjections([Memo1, Edit1]);
TCommandAction methods
| Utility method | Description |
|---|---|
WithCaption(aCaption) |
Sets an action caption which is displayed in a control |
WithShortCut(aShortcut) |
Sets a shortcut which is activating an action |
WithCommand(aCommand) |
Sets a command to execute |
WithInjections(aInjections) |
Injects values into the command's properties |
WithEventOnUpdate(aProc) |
Event triggered after action onUpdate event |
WithEventAfterExecution(aProc) |
Event triggered when command will be finished |
Sample setup OnUpdate event in TCommandAction:
Button2.Action := TCommandAction.Create(Self)
.WithCaption('Run sample command')
.WithCommand(MySampleCommand)
.WithEventOnUpdate(
procedure(cmd: TCommandAction)
begin
cmd.Enabled := CheckBox1.Checked;
end);
Command Evolution
TCommand Pattern allow developers to extract the valuable business code and make applications less coupled. Simultaneously developers can still use well known component practices and compose more complex code using command components. Developers can even expand Command Pattern with their own properties and events. However this approach is a temporary solution and should be evolved into more object oriented design.
TCommand Pattern is compatible to GoF Command Pattern (see diagrams above) and can be modernized. This moderation should be started when the refactoring phase will be finished and logic will be covered by unit tests. During refactoring all the visual dependencies should be removed, also all irrelevant dependencies and the code should be breaking down into smaller more logical methods or classes.
After modernization all dependencies should be inject through constructor, the command should be accessed through the interface, access to command internal items should be through getter and setter methods. Composed objects should be created using DI container, like Spring4D GlobalContainer method.
Samples
Ad-hoc command execution (create, inject, execute, remove)
TCommand.AdhocExecute<TSampleCommand>([Memo1, Edit1]);
Creates command and inject dependencies:
cmdSampleCommand := TSampleCommand.Create(AOwner);
cmdSampleCommand.Inject([Memo1, Edit1]);
Sample TCommand component:
type
TSampleCommand = class (TCommand)
private
FMemo: TMemo;
FEdit: TEdit;
protected
procedure DoGuard; override;
procedure DoExecute; override;
published
property Memo: TMemo read FMemo write FMemo;
property Edit: TEdit read FEdit write FEdit;
end;
procedure TSampleCommand.DoGuard;
begin
System.Assert(Memo<>nil);
System.Assert(Edit<>nil);
end;
procedure TSampleCommand.DoExecute;
begin
Memo.Lines.Add('Getting Edit text and put it here ...');
Memo.Lines.Add(' * Edit.Text: '+Edit.Text);
end;