Giter VIP home page Giter VIP logo

command-delphi's Introduction

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;

command-delphi's People

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar

command-delphi's Issues

Add TCommandAction.SetupEventOnUpdate

function TCommandAction.SetupEventOnUpdate (TProc<TCommandAction>): TCommandAction;

Button2.Action := TCommandAction.Create(Self)
  .SetupCaption('Run command: Button2')
  .SetupCommand(TSampleCommand.Create(Self))
  .SetupShotcut( TextToShortCut('CTRL+K') )
  .SetupEventOnUpdate(procedure (act: TComanndAction)
    begin
      act.Enable := (Memo1.Lines > 1)
    end)
  .InjectToCommand([Memo1, Edit1])
);

[TCommandAction] Rename SetupSomethig => WithSomethig

  1. Change methods of TCommandAction: SetupSomething => WithSomething
  2. Update documentation

Methods list:

  • SetupCaption => WithCaption
  • SetupCommand => WithCommand
  • SetupShortCut => WithShortCut
  • SetupEventOnUpdate => WithEventOnUpdate
  • SetupEventAfterExecution => WithEventAfterExecution
  • Inject => WithInjection

Property Injector API

variant A

// function InjectProperty(const APropertyName String; Value:Variant): TInjectRec;
Button2.Action := TCommandVclFactory.CreateCommandAction<TButon2Command>(Self,
  'Run command: Button2', [InjectProperty('Memo', Memo1),
  InjectProperty('Edit', Edit1)]);

variant B

// TIjector = class
//    function InjectProperty(APropertyName, Value): TIjector;
Button2.Action := TCommandVclFactory.CreateCommandAction<TButon2Command>(Self,
   'Run command: Button2', TIjector.Create.InjectProperty('Memo', Memo1)
   .InjectProperty('Edit', Edit1));

variant C

Button2.Action := TCommandVclFactory.CreateCommandAction<TButon2Command>(Self,
   'Run command: Button2', ['Memo','Edit'], [Memo1,Edit1]);

variant D

Button2.Action := TCommandVclFactory.CreateCommandAction<TButon2Command>(Self,
  'Run command: Button2', [['property.Memo',Memo1],['Edit',Edit1]]);

Separate TAsyncCommand and samples

TAsyncCommand unit:

  • Pattern.Command.pas ==> Pattern.AsyncCommand.pas

Extract:

  • TDiceRollCommand and TAsyncDiceRollCommand to separate demo

Update TCommand tests

Update properties in unit Tests.TCommandFactory
in class: TAdvancedCommand = class(TCommand)

  • use base classes TStrings and TStream
  • add isPrime method
  • add Flag injection: ProcessNonPrimeNumbers: boolean

Redesign TComand construction

Remove command construction code from: TCommandVclFactory class:
move to TCommand class

  1. Create command
    • TCommandVclFactory.CreateCommand<TMyCommand>(Owner,[injections...])
    • change to
    • TMyCommand.CreateAndInject(Owner,[injections...])
  2. Execute command
    • TCommandVclFactory.ExecuteCommand<TMyCommand>([injections...]);
    • change to
    • TCommand.Execute<TMyCommand>([injections...]);

Ignore published properties of TComponent in TClassPropertyList

When

  • getting metadata form TComponent based object with:
  TClassPropertyList.Create(AnyComponentObject);

Then

  • Check TComponent standard published properties:
  • Ignore all standard properties:
  • in Delphi 10.3 there are two such properties:
    1. Name: TComponentName
    2. Tag: NativeInt

Async Command - property ThreadSafeObject

type TAsyncCommand = class(TCommand)
  property ThreadSafeObject: TObject 
    read GetThreadSafeObject
    write SetThreadSafeObject;
function TAsyncCommand GetThreadSafeObject: TObject;
begin
  TMonitor.Enter(Self);
  try
    Result := fThreadSafeObject;
  finally
    TMonitor.Exit(Self);
  end;
end;

Update signature in components

Remove release date from:

  • Pattern.Command.pas

Change ReleaseVersion to Version in:

  • Pattern.Command.pas

Add siganture to:

  • TCommandAction.pas

Injection inherited class which base class is equal to property class

Allow to inject inherited class, example:

type
  TMyComponent = class(TComponent)
  strict private
    FStrings: TStrings;
  published
    property Strings: TStrings read FStrings write FStrings;
  end;

allow to inject:

  var aComponent := TMyComponent.Create(aOwner);
  var aStringList := TStringList.Create;
  TComponentInjector.InjectProperties(aComponent, [aStringList]);
  Assert (aComponent.Strings = aStringList);

Refactor project file names

DUnitxCommand.dprTestCommandPattern.dpr
Tests.TCommandFactory.pas ⇒ Tests.TCommand.pas

  • class: TestCommndFactory_BasicCommandTestBasicCommnd
  • class: TestCommndFactory_StrigListCommandTestStrigListCommnd
  • class: TestCommndFactory_AdvancedCommandTestAdvancedCommnd

Rename methods in TCommandAction

SetupCaption => WithCaption
SetupCommand => WithCommand
SetupShortCut => WithShortCut
SetupEventOnUpdate => WithEventOnUpdate
SetupEventAfterExecution => WithEventAfterExecution
Inject => WithInjection

Update samples and doc

Add Empty injecton as default value in the factory methods

class function CreateCommand<T: TCommand>(AOwner: TComponent;
const Injections: array of const): T;
class procedure ExecuteCommand<T: TCommand>(const Injections
: array of const);
class function CreateCommandAction<T: TCommand>(AOwner: TComponent;
const ACaption: string; const Injections: array of const): TAction;

type
  TCommandVclFactory = class(TComponent)
    // ...
    class function CreateCommand<T: TCommand>(AOwner: TComponent;
      const Injections: array of const = []): T;

Async command event OnProgress

TAsyncCommand = class (TCommand)
    procedure WithEventOnProgress(TProc)
    property ProgressInterval: integer;
    function GetElapsedTime: TTimeSpan;

Add diagnostics to TAsyncCommand:

  • uses System.Diagnostics
  • fStopwatch: TStopwatch;

DoGuard should be not mandatory to implement

procedure DoGuard; shouldn't be mandatory to implement in TCommand and in TAsyncCommand. Like in sample bellow:

type
  TSamapleCommand
  protected
    procedure DoExecute; override;
  end;

Document TAsynCommand

Updated main README with:

  • usage of TAsynCommand (general information)
  • sample implementation (copy this topic to doc ver 1.0)
  • sample execution (copy this topic to doc ver 1.0)

Add More injection unit tests

Requires #5 to be closed

Check:

  1. Expected System.EAssertionFailed exception if no required exection was provided (use TCommandStringList class)
  2. Command with two TStringList published properties (some variants: eg. change injection order)
  3. Command with two TStringList and TComponent published properties between

Command add ElapsedTime

type
  TCommand = class
    function GetElapsedTime: TTimeSpan;
    function GetElapsedTimeMs: Integer;

Add lock to TAsyncCommand.Execute

See TODO in comment:

procedure TAsyncCommand.Execute;
begin
  DoGuard;
  DoPrepare;
  fIsWorking := True;
  TThread.CreateAnonymousThread(
    procedure
    begin
      DoExecute;
      // TODO: lock or critical section is required bellow (critical !!!)
      fIsWorking := True;
    end);
end;

Async command add WithInjections

function TAsyncCommand.WithInjections(const Injections: array of const): TCommand;
begin
  TComponentInjector.InjectProperties(Self, Injections);
  Result := Self;
end;

Separate Tests into two classes

  • TCommandFactoryTests =>
    • TFactoryNoInjectionTest
    • TFactoryWithInjectionTest

(maybe saperate units too) - now is too early IMHO

Update README do version 1.0

(Review current documentation and add missing points)

List:

  1. Section [TCommand memory management]
    • TBD: Describe advantages of management base on TComponent solution using owner.
  2. Updated and expand TAsyncCommand documentation
    • Events list
    • sample
  3. TCommandAction refactoring:
    • SetupCaption => WithCaption
    • SetupCommand => WithCommand
    • SetupShortCut => WithShortCut
    • SetupEventOnUpdate => WithEventOnUpdate
    • SetupEventAfterExecution => WithEventAfterExecution
    • Inject => WithInjection
  4. TAsyncCommand events:
    • WithEventBeforeStart
    • WithEventAfterFinish
    • thread name for the debugging
      • fThread.NameThreadForDebugging('TAsyncCommand - '+Self.ClassName);
  5. TAsyncCommand - OnUpdate with TTimer
    • WithEventOnProgress(aProc)
    • property ProgressInterval: integer;
    • function GetElapsedTime: TTimeSpan;
  6. All Commands:
    • IsBusy function
    • GetElapsedTime / GetElapsedTimeMs
  7. TAsyncCommand.Terminate

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.