Last updated 23/05/2020
A previous post discussed the benefits of using the command pattern to provide a service class with a safe and convenient way to allow other objects to invoke and monitor a sequence of operations (a service). This post describes an implementation of the command pattern that uses a class (TExecution) that implements an interface (IExecution). The source code for an example project can be found here. The Execution classes and interfaces are defined in the library tcl_BaseClasses that are part of the project.
The TExecution class keeps track of the execution state of the service, and has methods to update the execution state and properties to show the current execution state in a convenient form. An instance of TExecution is declared as an instance variable of the service class, and an interface pointer of type IExecution is made available to the client code via a property
The execution state is held in the TExecution instance by an enumerated value (TExecutionState), defined as follows:
- Ready indicates the service is not currently executing.
- Starting indicates that there has been an external request to start the service, but the service has not yet been initiated.
- Busy indicates that the service is currently executing.
- Aborting indicates that there has been an external request to cancel the current service, which should be handled internally to cleanup and return to a ready state.
The state machine is shown below. States are shown in green, methods called by the client via the IExecution interface are shown in red and methods called by the service implementation are shown in blue:
The IExecution interface is the interface provided to the client code to allow it to start and abort the service, and to clear the results.
The following methods are implemented:
If the current state is Ready, the result flags are cleared and the current state is set to Starting to request execution of the service. True is returned to signal that the start request was accepted.
If the current state is not Ready, an event is raised to provide a notification that the state was not the expected state, and False is returned (see later for a description of the error and aborted events)
If the current state is not Ready, then the current state is set to Aborting. Otherwise, the abort is silently ignored.
This method provides the calling object with a way to interupt execution of the current operation and return it to a ready state. For this to work, the implementing class must detect the Aborting state and cancel its current operation. Once cancelled, it signals the completion of the abort using the SetAborted method which returns the current state to Ready
If the current state is Busy, an event is raised to provide a notification that the service was aborted (see later for a description of the error and aborted events)
The Clear method can be used to clear result flags, provided the current state is Ready. If the current state is not Ready, an event is raised to provide a notification that the state was not the expected state (see later for a description of the error and aborted events)
Methods not belonging to IExecution
The following methods are provided to allow the service class to update the current execution state. They are excluded from the IExecution interface as the state transitions they trigger need to be restricted to the service class implementation:
- SetBusy() – If the execution state is Starting, the Busy state is selected to indicate that the operation has started
- SetDone() – Sets the Done property and selects the Ready state to indicate that the operation has completed succesfully
- SetError(ErrorId) – Sets the Error and ErrorId properties and selects the Ready state to indicate that the operation failed to complete sucessfully. An event is raised to provide a notification that an error ocurred.
- SetAborted – Sets the Aborted property to indicate that the operation was aborted and selects the Ready state. An event is raised to provide a notification that the current service was aborted.
The following read-only properties are defined:
- Ready (BOOL)- current state is Ready
- Busy (BOOL) – current state is Busy
- Done (BOOL) – last opertion completed successfully
- Error (BOOL) – last operation terminated with an error
- ErrorId (UDINT) – error Id of last error
- Aborted (BOOL) – last operation was aborted
- Event_OnAbort – Interface pointer to the OnAbort event. This can be set to zero to inhibit the instance from raising the OnAbort event
- Event_OnError – Interface pointer to the OnError event. This can be set to zero to inhibit the instance from raising the OnError event
The IExecution interface that is available to the client class is exposed via a read-only property:
Because interfaces are reference types, we can access methods and properties directly on the interface property. i.e. External code can initiate the operation by calling Start() via the IExecution interface, and check the progress of the operation by querying the state of the Busy, Done and Error properties.
Every instance of TExecution has the ability to raise an event when its SetError or SetAborted method is executed, or when the Start method is executed while the execution state is not ready. This is very useful when testing the software on a machine. See this post for an explanation of the event framework.
The library that implement TExecution has a global variable list called ExecutionEvents. This holds a pair of events – Event_OnError and Event_OnAbort:
Each instance of TExecution holds references to these event instances, allowing them to raise the events as necessary. These reference is intialised in the FB_Init method of TExecution (and can be zeroed via the associated property to inhibit the event from being raised).
These events can be subscribed to by any external class that implements the following interfaces:
This only requires that the class has methods called OnExecutionFailed and OnExecutionAborted respectively. These methods would typically write a message to an event log or send a message to the error list in Visual Studio. For example, a TLogging class that implements the event handlers might look like this:
The Logging object is linked to the execution events as follows:
So with a few lines of code, all execution errors and aborts can be directed to a single destination for analysis.