Last updated 10/03/2022
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 (T_Execution) that implements an interface (I_Execution). The source code for an example project can be found here. The Execution classes and interfaces are defined in the library tcl_BaseClasses. The source code for the library can be found on git hub here
The T_Execution 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 T_Execution is declared as an instance variable of the service class, and an interface pointer of type I_Execution is made available to the client code via a property
The execution state is held in the T_Execution instance by an enumerated value (T_ExecutionState), 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 I_Execution interface are shown in red and methods called by the service implementation are shown in blue:
The I_Execution 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, a notification 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 notifications)
If the current state is not Aborted, then the current state is set to Aborting. Otherwise, the abort is silently ignored.
This method provides the calling object with a way to interrupt 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:
The SetAborted() method can also be used where the implementing class aborts the operation (for example, a motion operation may be aborted by the NC). So if the current state was Busy, a notification is raised to indicate that the service was aborted internally (see later for a description of the error and aborted notifications)
The Clear method can be used to clear result flags, provided the current state is Ready. If the current state is not Ready, a notification is raised to indicate that the state was not in the expected state (see later for a description of the error and aborted notifications)
Methods not belonging to I_Execution
The following methods are provided to allow the service class to update the current execution state. They are excluded from the I_Execution 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 successfully
- SetError(ErrorMsg) – Sets the Error and ErrorMsg properties and selects the Ready state to indicate that the operation failed to complete successfully. A notification is raised to provide a notification that an error occurred.
- SetAborted – Sets the Aborted property to indicate that the operation was aborted and selects the Ready state. A notification 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 operation completed successfully
- Error (BOOL) – last operation terminated with an error
- ErrorMsg (STRING) – error message describing the last error
- Aborted (BOOL) – last operation was aborted
- Notification_OnAbort – Interface pointer to the OnAbort notification. By default, this points to a global notification object in the GVL GlobalNotifications, but can be set to zero to inhibit the instance from raising the OnAbort notification
- Notification_OnError – Interface pointer to the OnError notification. By default, this points to a global notification object in the GVL GlobalNotifications, but can be set to zero to inhibit the instance from raising the OnError notification
The I_Execution 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 I_Execution interface, and check the progress of the operation by querying the state of the Busy, Done and Error properties.
Every instance of T_Execution has the ability to raise a notification 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 notification framework.
The library that implement T_Execution has a global variable list called GlobalNotifications. This holds a set of notifications:
Each instance of T_Execution holds references to these notification instances, allowing them to raise the notification as necessary. These references are intialised in the FB_Init method of T_Execution (and can be zeroed via the associated property to inhibit the notification from being raised).
These notifications can be handled by instances of T_NotificationHandler in an external class (a Logger class, for example) which can then be used to send error messages, etc:
The Logging object is linked to the execution notifications as follows:
So with a few lines of code, all execution errors and aborts can be directed to a single destination for analysis.
Client Execution Methods
The implementing class (that holds a T_Execution instance) can provide access to the execution object via a property of type I_Execution. This property can then be used to call the Start() method of the execution object, and to query the Done, Error and ErrorMsg properties of the execution object.
Where the implementing class requires some parameters to execute the service, this can be achieved by adding a method with the required parameters as inputs. This method writes the parameters to the implementing class then calls the Start method of the execution object. If this method returns an interface pointer to the execution class, this can then be used by the client code to query the state of the execution.
For example, in a class that implements a TCP client connection to a server we could have an execution object (_SendMessage) that controls the execution of the sending of a message to the server. Access to this execution object is via a public method SendMessage() that takes the message to send as a parameter. This method stores the message in an instance variable, then calls the Start method of the _SendMessage object to trigger the sending of message, and returns an interface pointer to the execution object.
This return value is stored by the calling class in a variable Result of type I_Execution, that can then be queried by the calling class to determine the result of the Send operation:
The T_Execution class can offer a consistent and safe way for classes to provide an API to the operations they implement. It can track the state of an operation, provide a mechanism for aborting the operation and returning the class to a known state, and can provide diagnostics across the entire program to track errors in the execution of operations.