Configuring via object construction

When configuring an application through object construction, the objective is to build the required objects at startup time. In effect, the configuration file defines the various initialization parameters for the objects that will be built.

We can often centralize much of this initial object construction in a single main() function. This will create the objects that do the real work of the application. We'll revisit and expand on these design issues in Chapter 18, Coping with the Command Line.

Let's now consider a simulation of Blackjack playing and betting strategies. When we run a simulation, we want to gather the performance of a particular combination of independent variables. These variables might include some casino policies including the number of decks, table limits, and dealer rules. The variables might include the player's game strategies for when to hit, stand, split, and double down. It could also include the player's betting strategies of flat betting, Martingale betting, or some more Byzantine type of betting system; our baseline code starts out like this:

import csv

def
simulate_blackjack() -> None:
# Configuration
dealer_rule = Hit17()
split_rule = NoReSplitAces()
table = Table(
decks=6, limit=50, dealer=dealer_rule,
split=split_rule, payout=(3, 2)
)
player_rule = SomeStrategy()
betting_rule = Flat()
player = Player(
play=player_rule, betting=betting_rule,
max_rounds=100, init_stake=50
)

# Operation
simulator = Simulate(table, player, samples=100)
result_path = Path.cwd() / "data" / "ch14_simulation.dat"
with result_path.open("w", newline="") as results:
wtr = csv.writer(results)
wtr.writerows(gamestats)

In this example, the Configuration part of the code builds the six individual objects to be used in the Operation phase. These objects include dealer_rule, split_rule, table, player_rule, betting_rule, and player. Additionally, there is a complex set of dependencies between table and subsidiary objects as well as player and two other objects.

The second part of the code, Operation, builds a Simulate instance using table and player. A csv writer object then writes rows from the simulator instance. This final writerows() function depends on the Simulate class providing a __next__() method.

The preceding example is a kind of technology spike – an initial draft solution – with hardcoded object instances and initial values. Any change is essentially a rewrite. A more polished application will rely on externally-supplied configuration parameters to determine the classes of objects and their initial values. When we separate the configuration parameters from the code, it means we don't have to tweak the code to make a change. This gives us consistent, testable software. A small change is accomplished by changing the configuration inputs instead of changing the code.

The Simulate class has an API that is similar to the following code:

from dataclasses import dataclass

@dataclass
class Simulate:
"""Mock simulation."""

table: Table
player: Player
samples: int

def __iter__(self) -> Iterator[Tuple]:
"""Yield statistical samples."""
# Actual processing goes here...

This allows us to build the Simulate() object with some appropriate initialization parameters. Once we've built an instance of Simulate(), we can iterate through that object to get a series of statistical summary objects.

The next version of this can use configuration parameters from a configuration file instead of the hardcoded class names. For example, a parameter should be used to decide whether to create an instance of Hit17 or Stand17 for the dealer_rule value. Similarly, the split_rule value should be a choice among several classes that embody several different split rules used in casinos.

In other cases, parameter values should be used to provide arguments to the Simulate class __init__() method. For example, the number of decks, the house betting limit, and the Blackjack payout values are configuration values used to create the Table instance.

Once the objects are built, they interact normally through the Simulate.__next__() method to produce a sequence of statistical output values. No further need of a global pool of parameters is required: the parameter values are bound into the objects through their instance variables.

The object construction design is not as simple as a global property map. While more complex, it has the advantage of avoiding a global variable, and it also has the advantage of making the parameter processing central and obvious in the main factory function.

Adding new parameters when using object construction may lead to refactoring the application to expose a parameter or a relationship. This can make it seem more complex than a global mapping from name to value.

One significant advantage of this technique is the removal of the complex if statements deep within the application. Using the Strategy design pattern tends to push decision-making forward into object construction. In addition to simplifying the processing, the elimination of the if statements means there are fewer statements to execute and this can lead to a performance boost.

In the next section, we will demonstrate how to implement a configuration hierarchy.

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

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