Skip to content

Latest commit

 

History

History
249 lines (189 loc) · 10.8 KB

README.md

File metadata and controls

249 lines (189 loc) · 10.8 KB

Command Pattern for Delphi

 Delphi Support  version

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:

  1. classic Gang of Four ICommand interface
  2. VCL TCommand class 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:

  1. The same object types are matched
  2. 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.
  3. More specific object passed as parameter is matching to more general object in properties list
  4. Numeric integer parameters are assigned to numeric properties
  5. Strings to strings
  6. 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

  1. Instant (ad-hoc) command execution
    • TCommand.AdhocExecute<T> - executes a command (creates a command, injects dependencies executes it and removes)
  2. Full command construction and execution
    • Create command with standard (component) constructor
    • Call method Inject
    • Execute command with Execute
  3. Build command invoker TCommandAction which executes the command when the action is invoked
    • TCommandAction class 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:

  1. Access to UI control fProgressBar: TProgressBar
  2. 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;