Designing command classes

Many applications involve an implicit Command design pattern; after all, we're processing data. To do this, there must be at least one active-voice verb, or command, that defines how the application transforms, creates, or consumes data. A simple application may have only a single verb, implemented as a function. Using the command class design pattern may not be helpful for simple applications.

More complex applications will have multiple, related verbs. One of the key features of GUIs and web servers is that they can do multiple things, leading to multiple commands. In many cases, the GUI menu options define the domain of the verbs for an application.

In some cases, an application's design stems from a decomposition of a larger, more complex verb. We may factor the overall processing into several smaller command steps that are combined in the final application.

When we look at the evolution of an application, we often see a pattern where new functionality is accreted. In these cases, each new feature can become a kind of separate command subclass that is added to the application class hierarchy.

An abstract superclass for commands has the following design:

class Command:

def __init__(self) -> None:
self.config: Dict[str, Any] = {}

def configure(self, namespace: argparse.Namespace) -> None:
self.config.update(vars(namespace))

def run(self) -> None:
"""Overridden by a subclass"""
pass

We configure this Command class by setting the config property to argparse.Namespace. This will populate the instance variables from the given namespace object.

Once the object is configured, we can set it to doing the work of the command by calling the run() method. This class implements a relatively simple use case, as shown in the following code:

    main = SomeCommand() 
    main.configure = some_config 
    main.run() 

This captures the general flavor of creating an object, configuring it, and then letting it do the work it was configured for. We can expand on this idea by adding features to the command subclass definition.

Here's a concrete subclass that implements a blackjack simulation:

class Simulate_Command(Command):
dealer_rule_map = {
"Hit17": Hit17, "Stand17": Stand17}
split_rule_map = {
"ReSplit": ReSplit, "NoReSplit": NoReSplit, "NoReSplitAces": NoReSplitAces
}
player_rule_map = {
"SomeStrategy": SomeStrategy, "AnotherStrategy": AnotherStrategy}
betting_rule_map = {
"Flat": Flat, "Martingale": Martingale, "OneThreeTwoSix": OneThreeTwoSix
}

def run(self) -> None:
dealer_rule = self.dealer_rule_map[self.config["dealer_rule"]]()
split_rule = self.split_rule_map[self.config["split_rule"]]()
payout: Tuple[int, int]
try:
payout = ast.literal_eval(self.config["payout"])
assert len(payout) == 2
except Exception as e:
raise Exception(f"Invalid payout {self.config['payout']!r}") from e
table = Table(
decks=self.config["decks"],
limit=self.config["limit"],
dealer=dealer_rule,
split=split_rule,
payout=payout,
)
player_rule = self.player_rule_map[self.config["player_rule"]]()
betting_rule = self.betting_rule_map[self.config["betting_rule"]]()
player = Player(
play=player_rule,
betting=betting_rule,
max_rounds=self.config["rounds"],
init_stake=self.config["stake"],
)
simulate = Simulate(table, player, self.config["samples"])
with Path(self.config["outputfile"]).open("w", newline="") as target:
wtr = csv.writer(target)
wtr.writerows(simulate)

This class implements the essential top-level function that configures the various objects and then executes the simulation. This class refactors the simulate_blackjack() function shown previously to create a concrete extension of the Command class. This can be used in the main script, as shown in the following code:

if __name__ == "__main__": 
    with Setup_Logging(): 
        with Build_Config(sys.argv[1:]) as config:     
            main = Simulate_Command() 
            main.configure(config)
            main.run() 

While we could make this command into Callable and use main() instead of main.run(), the use of a callable can be confusing. We're explicitly separating the following three design issues:

  • Construction: We've specifically kept the initialization empty. In a later section, we'll show you some examples of PITL, where we'll build a larger composite command from smaller component commands.
  • Configuration: We've put the configuration in via a property setter, isolated from the construction and control.
  • Control: This is the real work of the command after it's been built and configured.

When we look at a callable or a function, the construction is part of the definition. The configuration and control are combined into the function call itself. We sacrifice a small bit of flexibility if we try to define a callable.

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

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