Designing a higher-level, composite command

We can also design a composite command that's built from other commands. For this, we have two design strategies: object composition and class composition.

If we use object composition, then our composite command is based on the built-in list or tuple. We can extend or wrap one of the existing sequences. We'll create the composite Command object as a collection of instances of other Command objects. We might consider writing something like the following code:

simulate_and_analyze = [Simulate(), Analyze()] 

This has the disadvantage that we haven't created a new class for our unique composite command. We created a generic composite and populated it with instances. If we want to create even higher-level compositions, we'll have to address this asymmetry between low-level Command classes and higher-level composite Command objects based on built-in sequence classes.

We'd prefer to have a composite command that was also a subclass of a command. If we use class composition, then we'll have a more consistent structure for our low-level commands and our higher-level composite commands.

Here's a class that implements a sequence of other commands:

class Command_Sequence(Command):
steps: List[Type[Command]] = []

def __init__(self) -> None:
self._sequence = [class_() for class_ in self.steps]

def configure(self, config: argparse.Namespace) -> None:
for step in self._sequence:
step.configure(config)

def run(self) -> None:
for step in self._sequence:
step.run()

We defined a class-level variable, steps, to contain a sequence of command classes. During the object initialization, __init__() will construct an internal instance variable, _sequence, with objects of the named classes in self.steps.

When the configuration is set, it will be pushed into each constituent object. When the composite command is executed via run(), it is delegated to each component in the composite command.

Here's a Command subclass built from two other Command subclasses:

class Simulate_and_Analyze(Command_Sequence): 
    steps = [Simulate_Command, Analyze_Command] 

This class is only a single line of code to define the sequence of steps. As this is a subclass of the Command class itself, it has the necessary polymorphic API. We can now create compositions with this class because it's compatible with all other subclasses of Command.

We can now make the following very small modification to the argument parsing to add this feature to the application:

parser.add_argument(
"command", action="store", default='simulate',
choices=['simulate', 'analyze', 'simulate_analyze']
)

We simply added another choice to the argument option values. We'll also need to tweak the mapping from the argument option string to the class, as follows:

command_map = {
'simulate': Simulate_Command,
'analyze': Analyze_Command,
'simulate_analyze': Simulate_and_Analyze}

Note that we shouldn't use a vague name such as both to combine two commands. If we avoid vagueness, we create opportunities to expand or revise our application. Using the Command design pattern makes it pleasant to add features. We can define composite commands, or we can decompose a larger command into smaller subcommands.

Packaging and implementation may involve adding an option choice and mapping that choice to a class name. If we use a more sophisticated configuration file (see Chapter 14, Configuration Files and Persistence), we can provide the class name directly in the configuration file and save the mapping from an option string to a class.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset