21 The Command Framework

The Command Framework is a general abstraction for synchronous and asynchronous operations. It aims at simplifying and streamlining the creation of asynchronous operations and grouping them for sequential or parallel execution.

Many operations in Flex and Flash operations are asynchronous, as AS3 is not multithreaded and there are no blocking calls. Most of these operations come with their own APIs and their own particular set of events that mark completion or failure of its execution. Chaining several of these operations together usually requires a lot of plumbing that distracts from the actual business logic. With Spicelib Commands it is easier to keep each operation in a clean self-contained unit and easily chain them together with a convenient, fluent builder API.

The Commands Framework is a replacement for the Task Framework that was part of Spicelib 1 and 2. It is intended to be simpler to use and more powerful at the same time. The Task Framework is discontinued and no longer part of Spicelib 3.

21.1 Implementing a Command

The implementation of a command follows simple naming conventions. It does not require to extend a framework base class or implement a framework interface. There are two major reasons for this design decision. The first is to keep command implementations very simple and lightweight and useful even when used without the framework. The second is to keep the method signatures flexible to allow to pass data and callbacks to the execute method, as you'll see in the examples. The only downside is a negligible loss in type-safety.

The style of command implementation shown in this section is just the default. The documentation focuses on this style as it is very straightforward and should be sufficient for most use cases. If you are picky about details, prefer to code against interfaces or base classes instead, have special requirements or are a framework developer who needs additional functionality you can read 21.5 Extending the Framework for an overview over alternatives.

21.1.1 Synchronous Commands

A synchronous command may look as simple as this:

public class SimpleCommand {

    public function execute (): void {
    
        trace("I execute, Therefore I am");
        
    }
    
}

The only naming convention a synchronous command needs to adhere to, is to have a public method called execute. It may have parameters (see 21.3.4 Passing Data to the Execute Method) or a return type (see 21.1.5 Producing a Result), but both are optional.

21.1.2 Asynchronous Commands

An asynchronous command has to accept a callback function either as a method parameter or a public property. It invokes the callback method to signal command completion.

Callback as a Method Parameter

Any parameter in the execute method that is of type Function is interpreted as being the callback for an asynchronous command. The ordering does not matter, if you also want the framework pass data from previous commands the callback may be at any position in the parameter list:

public class MyAsyncCommand {

    public function execute (callback: Function): void {
    
        callback(true);
        
    }
    
}

In the example above the actual execution is synchronous, since the callback is invoked immediately. This is legal, as it allows to implement commands with transparent caching for example (if the result is already available, return it immediately, otherwise call some service asynchronously). See the next section for an example for such a command.

Callback as a Public Property

When you start an asynchronous operation, you usually want to keep the callback as a reference and invoke it later. You can store it in a property manually when receiving it as a method parameter in the execute method, but in most cases it is more convenient to get it injected into a property:

public class MyAsyncCommand {

    public var callback:Function;

    public function execute (): void {
    
        // rest of the implementation omitted for clarity:
        
        var result:Object = getResultFromCache();
        
        if (result) {
            callback(result);
        }
        else {
            getService().fetchData(resultHandler, errorHandler);
        }        
    }
    
    private function resultHandler (result: Object): void {
        callback(result);
    }
    
    private function errorHandler (error: ErrorEvent): void {
        callback(error);
    }
    
}

The naming convention requires that the public property is called callback. In the example above the callback gets invoked immediately when cached data is available, otherwise the result and error handlers invoke it later. As you see they both just pass the result or error instance to the callback, see the next section to understand how the framework interprets these parameters.

How to Invoke the Callback

The callback invocation is interpreted differently depending on whether you pass a parameter and what type it is of. Each asynchronous command can have three different final states after execution, and they are interpreted by the callback as follows:

Per default only Error or ErrorEvent instances are interpreted as an error outcome, but the list can be extended with custom error types:

LightCommandAdapter.addErrorType(FaultEvent);

Here we add the Flex FaultEvent as an error type. Since Spicelib Commands is a pure AS3 library it does not know about Flex classes. The LightCommandAdapter is the class that adapts the type of command demonstrated in this chapter to the framework interfaces.

When the command does not produce a result, it is recommended to simply pass true to the callback, as no parameter is interpreted as cancellation.

Invoking the callback with more than one parameter is illegal and leads to an error. A function reference is quite weak in terms of type-safety, but it allows to keep the command implementation decoupled from the framework APIs. Later releases might alternatively allow the injection of a concrete type (e.g. CommandCallback) with a more explicit, typed API.

21.1.3 Error Handling

If you want to signal an error condition instead of successful completion you can do that for synchronous and asynchronous commands in two different ways:

Synchronous Commands

public class FaultyCommand {

    public function execute (): void {
    
        throw new Error("Sorry, I do not function properly");
        
    }
    
}

A synchronous command may simply throw an error. If it is executed in a sequence or flow this will lead to the configured error handling and potentially cancel the sequence or flow with an error event.

Asynchronous Commands

public class FaultyCommand {

    public var callback:Function;

    public function execute (): void {
    
        getService().fetchData(resultHandler, errorHandler);
    }
    
    [...]
    
    private function errorHandler (error: ErrorEvent): void {
        callback(error);
    }
    
}

For an asynchronous command an error condition can be signalled by passing a parameter type which is interpreted as an error like already shown in the previous section.

21.1.4 Command Cancellation

An asynchronous command can support cancellation. There are two ways to cancel a command: from within by invoking the callback without parameters, like already shown in preceding sections, or from the outside when the command has a method called cancel.

Cancellation from within the Command

public class MyCancellableCommand {

    public var callback:Function;
    
    public function execute (): void {
    
        callback(); // invocation without parameters interpreted as cancellation
        
    }
    
}

Cancellation from the Outside

public class MyCancellableCommand {

    public var callback:Function;
    
    public function execute (): void {
    
        // start operation
        
    }
    
    public function cancel (): void {
    
        // cancel operation
        
    }
    
}

Here the command adds a method called cancel. If the command is part of a sequence or flow which gets cancelled, the framework will invoke the cancel method so that you can stop whatever asynchronous operation you started. When the active command in a sequence does not have a cancel method, the entire sequence cannot be cancelled and invocation of its cancel method leads to an error. Still, adding a cancel method is entirely optional.

When a command gets cancelled from the outside there is no need to invoke the callback to signal cancellation.

Note that such a cancel method is only meant to be called by the framework. Invoking it directly in your application code does not have any effect as the framework cannot intercept these calls.

21.1.5 Producing a Result

Any command (both synchronous and asynchronous) can produce a result. This is particularly useful as the result value can get injected into subsequent commands (either to their constructor or their execute method) and also to result handlers you add to command executors (see the next section).

Synchronous Commands

A synchronous command can produce a result through a return value:

public class CommandWithResult {

    public function execute (): String {
    
        return "Hello World!";
        
    }
    
}

The result can be of any type.

Asynchronous Commands

An asynchronous command can produce a result through passing a value to the callback function:

public class MyAsyncCommand {

    public var callback:Function;

    public function execute (): void {
    
        getService().fetchData(resultHandler);
        
    }
    
    private function resultHandler (result: Object): void {
        callback(result);
    }
    
}

Again, the result can be of any type, except for the few interpreted as an error (like Error and ErrorEvent).

21.1.6 Executing a Command

The framework comes with a fluent API to create commands, add result and error handlers, specify timeouts and execute them. For a single command this is usually not particularly useful, as you could as well just instantiate and invoke them yourself. But even for a single command it allows to wrap features like timeouts around your command without the need to deal with this type of functionality in the command implementation.

Commands.wrap(new MySimpleCommand())
    .timeout(10000)
    .result(resultHandler)
    .error(errorHandler)
    .execute();

private function resultHandler (result: Object): void {
    [...]
}

private function errorHandler (cause: Object): void {
    [...]
}

The real power of this API though is grouping commands for sequential or parallel execution or to command flows where subsequent commands are determined by dynamic links that interpret the result of the preceding command. You can find examples for how to execute these command types in 21.2 Command Groups and 21.4 Command Flows.

21.2 Command Groups

The real power behind the concept of abstracting asynchronous operations becomes apparent when you want to execute more than just one command. Spicelib Commands allow for grouping commands for parallel or sequential execution. There is also an advanced grouping mode called command flows that uses dynamic decision points between commands. See 21.4 Command Flows for details. This chapter only covers sequential and parallel execution.

21.2.1 Command Sequences

A simple sequence with 2 commands can be created and executed like this:

Commands
    .asSequence()
    .add(new LoginCommand())
    .add(new LoadUserProfileCommand())
    .lastResult(resultHandler)
    .error(errorHandler)
    .execute();
    
private function resultHandler (result: UserProfile): void {

    [...]
    
}

private function errorHandler (failure: CommandFailure): void {
    
    trace("Command " + failure.target + " failed, cause: " + failure.cause);
    
}

The fluent API allows to declare the commands and handlers of the group and execute it in one statement.

The error handler always has to accept a parameter of type CommandFailure. This is a type that neither extends Error nor ErrorEvent. It allows to inspect the target command in the group that failed as well as the cause (usually an instance of type Error nor ErrorEvent, but potentially something else).

The result handler above is only interested in the last result produced in that sequence. This might be quite common for sequences, in particular in cases where the result of previous commands get injected into and processed by subsequent commands (see 21.3 Passing Data to Commands for details).

Alternatively all results produced by the sequence may get inspected:

Commands
    .asSequence()
    .add(new LoadContactsCommand())
    .add(new LoadUserProfileCommand())
    .allResults(resultHandler)
    .execute();
    
private function resultHandler (result: CommandData): void {

    trace("Contacts: " + result.getObject(Contacts));
    trace("Profile: " + result.getObject(UserProfile));
    
}

21.2.2 Parallel Command Execution

Less common than sequences, but still quite handy to have when needed. The syntax is identical expect for calling inParallel instead of asSequence:

Commands
    .inParallel()
    .add(new LoadContactsCommand())
    .add(new LoadUserProfileCommand())
    .allResults(resultHandler)
    .error(errorHandler)
    .execute();

The rules for result and error handlers are the same as for sequences.

21.2.3 Lazy Command Instantiation

Instead of passing existing instances to add, you can alternatively just specify the command class. This way the instantiation will be deferred until the command actually gets used. This might be useful for flows (where some commands may never get executed) or when you want to pass results of preceding commands to the constructor of a subsequent one, which is only possible if the framework creates the instance for you.

Commands
    .asSequence()
    .create(LoginCommand)
    .create(LoadUserProfileCommand)
    .lastResult(resultHandler)
    .error(errorHandler)
    .execute();

21.2.4 Timeouts and Delayed Execution

Command Groups allow for the same set of optional features like the methods for executing a single command:

Commands
    .asSequence()
    .delay(1000)
    .add(new LoginCommand())
    .add(new LoadUserProfileCommand())
    .timeout(30000)
    .lastResult(resultHandler)
    .error(errorHandler)
    .execute();

In case of sequences the delay can be placed anywhere in the sequence, before the first or between two commands.

21.2.5 Combining the APIs for Single and Grouped Commands

Sometimes you may want to use the API for dealing with a single command to define something specific to that instance and then add it as part of a sequence or flow. In this case you can simply call build in the end instead of execute, which just gives you a command instance with those extra features applied, but without actually executing it. You can then add it to any group of commands:

var login:Command = Commands
    .wrap(new LoginCommand())
    .timeout(30000)
    .result(someHandlerOnlyForThisCommand)
    .build();
    
Commands
    .asSequence()
    .add(login)
    .add(new LoadUserProfileCommand())
    .lastResult(resultHandler)
    .error(errorHandler)
    .execute();

21.3 Passing Data to Commands

Often all commands executed in a sequence are self-contained and do not need to know each other. But sometimes the result of one command is needed when executing one of the subsequent commands. Preferrably this happens without the subsequent command needing any kind of knowledge about the type of the command that produced the result or any of its implementation details. This section provides an overview over the available options for passing results in a decoupled way.

21.3.1 Using a Shared Model Instance

When the commands executed in a sequence are all relatively close in a sense that they belong to the same functional area of the application, the most straightforward way is often to use a shared application-specific model instance that multiple commands have access to:

var model:LoginModel = new LoginModel();
Commands
    .asSequence()
    .add(new LoginCommand(model))
    .add(new LoadUserProfileCommand(model))
    .lastResult(resultHandler)
    .error(errorHandler)
    .execute();

Here the model gets passed to the constructor of both command instances. The first one can pass the result to the model before dispatching the complete event, so that the second instance has access to it when it gets executed.

For objects from the same functional area where decoupling commands from each other is not a concern, this might sometimes be the best approach, as it is very easy to use and does not require any help from the framework.

21.3.2 Data Passed by the Framework

When you want to keep commands decoupled in cases where the tasks they perform are largely unrelated, you can rely on the framework to pass the results for you. There are multiple different approaches available as described below. Note that although this is based on some sort of injection, it does not require an IOC container. This section still only describes the capabilities of the standalone Spicelib Commands project.

21.3.3 Passing Data into the Constructor

Any result from a command that has already been completed as part of a sequence or flow can be passed to the constructor of a subsequent command:

function MyCommand (user:User, config:XML) {
    this.user = user;
    this.config = config;
}

The logic for finding the result to inject is fairly simple: the framework looks for the result by type (reflecting on the constructor argument types), and if there is more than one matching type picks the one that was added last. This should already cover most real-world requirements. If you need more complex result lookup capabilities, you can still do explicit lookups as described further below.

If no matching result is found, the sequence will abort with an error, unless you marked the parameter as optional:

function MyCommand (user:User, config:XML = null) {

It should be obvious that constructor injection can only work when you pass the type of the command to the create method instead of an existing instance, as you need to leave it up to the framework to instantiate the command for you in this case:

Commands
    .asSequence()
    .create(LoginCommand)
    .create(LoadUserProfileCommand)
    .create(MyCommand)
    .lastResult(resultHandler)
    .error(errorHandler)
    .execute();

It is recommended to only use constructor injection when you target Flash Player 10.1 or newer, as older players had a nasty reflection bug that always reported * as the parameter type when you reflected before the VM created the first instance of that class.

21.3.4 Passing Data to the Execute Method

Alternatively results from preceding commands can also get injected into the execute method:

public function execute (user: User, config: XML): void {
    
    [...]
    
}

The rules are the same as for constructor injection: the framework looks for the result by type (reflecting on the parameter types), and if there is more than one matching type picks the one that was added last.

Obviously this type of injection also works for existing command instances:

Commands
    .asSequence()
    .add(LoginCommand)
    .add(LoadUserProfileCommand)
    .add(MyCommand)
    .lastResult(resultHandler)
    .error(errorHandler)
    .execute();

21.3.5 Passing Data Programmatically

Sometimes you want to be flexible. You might want to expect a User instance as a parameter of the execute method, and in some cases it might come from a preceding login command, while in other cases it might already be available. In the latter case you can manually pass data when wiring up the commands:

var user:User = ...;

Commands
    .asSequence()
    .add(LoadUserProfileCommand)
    .add(MyCommand)
    .data(user)
    .lastResult(resultHandler)
    .error(errorHandler)
    .execute();

The outcome is the same as in previous examples where the User instance was produced by a login command. The data method in the builder API can be invoked multiple times.

21.4 Command Flows

Command Flows add the concept of decision points to define a dynamic sequence of commands. The command builder API offers several declarative means of defining decision points based on result type, value or property value to cover the most common scenarios as well as a way to add a custom link instance in case some other type of decision logic is required. A command link simply determines the next command to execute based on the result of the previous command.

21.4.1 Linking by Result Type

Let's show a simple example where you want to execute the command to load the admin console of your application only when the user that just logged in is indeed an administrator. This example assumes that the instance returned from the server is different depending on the role of the user:

var profileLoader:Command = new ProfileLoaderCommand("some/serviceUrl");

var flow:CommandFlowBuilder = Commands.asFlow();

flow.add(new LoginCommand())
    .linkResultType(AdminUser).toCommandType(LoadAdminConsoleCommand)
    .linkResultType(User).toCommandInstance(profileLoader);
        
flow.create(LoadAdminConsoleCommand)
    .linkAllResults().toCommandInstance(profileLoader);
    
flow.timeout(30000)    
    .lastResult(resultHandler)
    .error(errorHandler)
    .execute();

We use the linkResultType method to branch to different command types based on the type of the result produced by the LoginCommand.

21.4.2 Linking by Result Value

You may not always have commands that finish with a different result type for all possible outcomes. In these cases you can alternatively link by value and not by class and use something like String constants that the commands may set as a result:

var profileLoader:Command = new ProfileLoaderCommand("some/serviceUrl");

var flow:CommandFlowBuilder = Commands.asFlow();

flow.add(new LoginCommand())
    .linkResultValue(MyConstants.ADMIN_LOGIN).toCommandType(LoadAdminConsoleCommand)
    .linkResultValue(MyConstants.USER_LOGIN).toCommandInstance(profileLoader);
        
flow.create(LoadAdminConsoleCommand)
    .linkAllResults().toCommandInstance(profileLoader);
    
flow.add(profileLoader)
    .linkAllResults().toFlowEnd();
    
flow.timeout(30000)    
    .lastResult(resultHandler)
    .error(errorHandler)
    .execute();

21.4.3 Linking by Result Property

If the result type is always the same, but carries a property value that can be used to determine the next command, the following syntax can be used:

var profileLoader:Command = new ProfileLoaderCommand("some/serviceUrl");

var flow:CommandFlowBuilder = Commands.asFlow();

flow.add(new LoginCommand())
    .linkResultProperty("isAdmin", true).toCommandType(LoadAdminConsoleCommand)
    .linkResultProperty("isAdmin", false).toCommandInstance(profileLoader);
        
flow.create(LoadAdminConsoleCommand)
    .linkAllResults().toCommandInstance(profileLoader);

flow.add(profileLoader)
    .linkAllResults().toFlowEnd();
       
flow.timeout(30000)    
    .lastResult(resultHandler)
    .error(errorHandler)
    .execute();

Here the login command will always return an instance of User, but the isAdmin property lets us know whether she has the admin role:

public class User {

    public var isAdmin: Boolean;
    
    [...]
    
}

21.4.4 Linking to the End of the Flow

Like already shown in all preceding examples, a link can explicitly point to the end of the flow:

flow.add(profileLoader)
    .linkAllResults().toFlowEnd();

Since the default behaviour of a command flow in case no link matches the result is to cancel the flow, the end of the flow has to be specified explicitly to reach successful completion of the flow.

21.4.5 Specifying Fallback Links

Whenever none of the specified links match it is interpreted as cancellation of the flow. Alternatively you can also specify a catch-all link like this:

flow.add(new LoginCommand())
    .linkResultType(AdminUser).toCommandType(LoadAdminConsoleCommand)
    .linkResultType(User).toCommandInstance(profileLoader);
    .linkAllResults().toCommandType(InitGuestModeCommand);

Here the final link is only processed if the first two links both do not match the results.

21.4.6 Custom Links

The examples above covered a more declarative way of linking commands that should be fine for many real world scenarios. Nevertheless sometimes you'd require custom logic. The best way to add a very simple condition is an inline function:

flow.add(new LoginCommand())
    .linkFunction(function (result: CommandResult, processor: CommandLinkProcessor): void {
        if (event.result is User && User(event.result).loginCount < 3) {
            processor.executeCommand(create(ShowNewUserDashboardCommand));
        }
        else {
        	processor.executeCommand(create(LoadUserProfileCommand));
        }
    });
    
    private function create (commandType: Class): Command {
        return Commands.create(commandType).build();
    }

The signature of the method is the same as for the link method in the CommandLink interface. Instead of specifying the next command to execute you can alternatively trigger the successfull completion of the flow, an Error or flow cancellation, using the methods of the CommandLinkProcessor instance.

Finally, if the logic is more complex and it is justified to extract it into a separate class you can also use any implementation of the CommandLink interface:

flow.add(new LoginCommand()).link(new MyCustomLink());

The linkFunction and link methods produce the only type of links that even get processed when the preceding command finished with an Error. This way even different error conditions can be linked if required. The other declarative ways of linking like the linkResultType or linkResultProperty methods are only considered when the preceding command completed successfully.

21.5 Extending the Framework

The preceding sections primarily covered the basic functionality of the library. It should be sufficient for most real world scenarios. This chapter gives an overview over available extension points. It is only relevant if you want to tweak Spicelib Commands to your needs or want to integrate it into another framework (like an IOC container for example).

The two major extension points are command adapters and the lifecycle hooks.

21.5.1 Implementing a Command Adapter

Adapters usually serve one of the following two purposes:

To integrate a new adapter the following steps must be performed:

The remaining sections explain these steps.

Implementing the CommandAdapter interface

To make your life easier you can extend the AbstractSuspendableCommand base class that covers some of the basic plumbing and then implement the CommandAdapter interface on top of it.

Your adapter then must override the doExecute method to execute the target method and then invoke the protected complete or error methods when the target command finished its execution. If your adapter supports cancellable or suspendable commands, it is also supposed to override the doCancel and/or doSuspend and doResume methods.

The default command type of the library is itself based on an adapter. If you want to study the implementation as an example you can browse the code of the LightCommandAdapter class.

Implementing the CommandAdapterFactory interface

This class is responsible to actually create new adapter instances based on an already existing target command instance. The interface is simple:

public interface CommandAdapterFactory {

    function createAdapter (instance:Object, domain:ApplicationDomain = null) : CommandAdapter;
    
}

Whenever a command instance that does not already implement one of the Command interfaces is added to one of the executors created by the various command builder APIs, the framework consults all available command adapter factories to try to turn the command instance into an adapter instance. The adapter factories will be executed one after the other until one of them returns an adapter (Chain of Responsibility pattern). In case your adapter factory does not "recognize" the provided instance it should return null to signal to the framework that it should ask the next adapter. If no factory feels responsible for a command instance, an error will be thrown.

Registering a CommandAdapterFactory

Finally the factory must be registered with the framework:

CommandAdapterFactories
    .addFactory(new MyFactory())
    .order(1);

The order determines which factory gets asked in which order in case there are multiple factories registered.

21.5.2 Implementing a Result Processor

A result processor can be registered to process certain types of results. This allows to transparently modify or transform these result types without the need of the command itself being aware of this kind of processing.

A result processor itself must be implemented as a command. The class can then get registered centrally, causing the framework to create a new instance of that result processor command for each matching result produced by some other command.

This section will show the AsyncToken support from Parsley as an example. The Spicelib Commands library does not know about AsyncTokens as it does not depend on the Flex SDK and can be used in pure Flash applications, too.

A result processor for an AsyncToken works around the fact that the moment a command returns an AsyncToken the actual result is not available yet. It removes the need for plumbing around Responders inside the command itself, if we move this taks to an external result processor.

The implementation of this processor looks like this:

public class AsyncTokenResultProcessor {
    
    
    private var active: Boolean;
    private var callback: Function;
    
    
    public function execute (token: AsyncToken, callback: Function): void {
        this.callback = callback;
        active = true;
        token.addResponder(new Responder(result, fault));
    }
    
    private function result (event: ResultEvent): void {
        if (!active) return;
        callback(event.result);
        active = false;
    }
    
    private function fault (event: FaultEvent): void {
        if (!active) return;
        callback(event.fault);
        active = false;
    }
    
    public function cancel (): void {
        active = false;
    }
    
    
}

The AsyncToken produced by any other command will get passed to the execute method of this processor. It then adds a responder and waits for either a result or a fault to be returned. In both cases the result or the fault are then simply passed to the callback.

To register this processor only one more line of code is needed (must be executed before the first command is started):

ResultProcessors.forResultType(AsyncToken).processorType(AsyncTokenResultProcessor);

In this special case we also must tell the framework that an instance of Fault signals an error condition (again Spicelib does not depend on the Flex SDK, so it does not know about Faults):

LightCommandAdapter.addErrorType(Fault);

Without this registration, Faults would simply be interpreted as successful results.

With all these pieces in place, a command based on an AsyncToken can then look as simple as this:

public class GetUserListCommand {

	private var service: RemoteObject;

	function GetUserListCommand (service: RemoteObject) {
	    this.service = service;
	}
	

    public function execute (): AsyncToken {
    
    	return service.getUserList();
        
    }
    
}

The result processor would kick in as soon as this command returns the AsyncToken and treat the result produced of this processor as the final result, not the AsyncToken.

Note that if you are using Spicelib Commands in Parsley and add the parsley-flex.swc, the result processor shown above will be registered automatically.

Similarly you could create result processors for URLLoader objects or other asynchronously executing objects.

But result processors can also be project-specific, processing or transforming command results centrally.

21.5.3 Using the Lifecycle Hooks

Another extension point provided by the library are the lifecycle hooks. They are particularly useful when you want to integrate the Spicelib Commands into a different kind of framework, like an IOC container for example. The Parsley framework is using these hooks in version 3 for the redesigned command support. But other containers can integrate in the same way.

For integration into a container, usually the following functionality is desirable:

To make this work the CommandExecutor interface contains a method called prepare that allows to pass customized implementations of the CommandData and CommandLifecycle interfaces down to the executed commands:

public interface CommandExecutor extends SuspendableCommand {
    
    function prepare (lifecycle:CommandLifecycle, data:CommandData) : void;
    
    [...]
    
}

All built-in executors like those for executing command flows or sequences implement this interface. When the prepare method is invoked before executing the flow or sequence the specified CommandLifecycle and CommandData instances will get passed down to all individual commands.

The CommandLifecycle Interface

The interface looks like this:

public interface CommandLifecycle {
    
    function createInstance (type:Class, data:CommandData) : Object;
    
    function beforeExecution (command:Object, data:CommandData) : void;
    
    function afterCompletion (command:Object, result:CommandResult) : void;
    
}

It is responsible for creating command instances, and it may execute additional logic before and after execution of the command. The beforeExecution and afterExecution methods are the hooks that can be used for adding and removing commands to a container.

The CommandData Interface

Finally the CommandData interface can be used to integrate the simple injection features of Spicelib Commands (that allow for injection of results produced by preceding commands) with the injection facility of a container.

public interface CommandData {
    
    function getObject (type:Class = null) : Object;
     
    function getAllObjects (type:Class = null) : Array;
     
}

These methods will get invoked by the framework when an execute method or a command constructor expects a specific type to get injected. Your custom CommandData implementation will only get invoked when the framework cannot find a matching type itself (e.g. from the results of preceding commands). This allows for seamless integration.

Putting it all together

Once you have your custom CommandLifecycle and CommandData implementations you may want to create a nice builder API so that your users do not need to care about these low-level details. This builder API might be similar to the one built into Spicelib. It would allow to create, group and execute commands and under the hood it would silently create instances of the lifecylce and data implementations and pass them to the prepare method of your executor.