16.6 大规模程序设计
让我们在21点模拟程序中添加一个功能:分析结果。我们有许多方式来实现这个新添加的功能。我们的考虑包括两个维度,这带来了大量的组合。考虑其中一个维度是如何设计新功能。
- 添加一个函数。
- 使用命令模式。
另一个维度是如何包装新的功能。
- 编写一个新的顶层脚本文本。我们会基于文件的名称,比如simulate.py和analyze.py,创建新的命令。
- 添加一个新的参数到应用程序中允许脚本执行模拟或者分析功能。我们会有类似于app.py simulate和app.py analyze的命令。
这4种组合都是实现这个功能的合适方式。我们会专注于使用命令设计模式。首先,我们会将现有的应用程序修改为使用命令设计模式。然后,会通过添加功能的方式扩展应用程序。
16.6.1 设计命令类
许多应用程序都隐式使用了命令设计模式。毕竟,我们在处理数据。为了使用这种模式,至少需要一个用于定义应用程序如何转换、创建或者使用数据的主动词(active-voice verb)。简单的应用程序可能只包含一个实现为一个函数的动词。这种情况下,使用命令类设计模式可能没有帮助。
更复杂的应用程序会包含多个相关的动词。GUI应用程序和Web服务器的一个主要功能就是它们可以完成多项工作、执行多个命令。在许多情况下,GUI的菜单选项定义了应用程序的动词领域。
在一些情况下,设计应用程序是从分解一个更大、更复杂的动词开始的。我们可以把全局的处理过程分解为几个更小的命令步骤,然后将这些步骤合并成最终的应用程序。
当研究应用程序的演变时,我们经常会看到这样一种模式——新的功能与当前应用程序合并。在这些情况下,每个新的功能都可以成为添加到应用程序类层次中的一种独立的命令子类。
下面是抽象的命令基类。
class Command:
def setconfig( self, config ):
self.dict.update( config._dict )
config= property( fset=set_config )
def run( self ):
pass
我们通过将config属性设置为types.SimpleNamespace或者argparse. Namespace,甚至是另外一个Command实例来配置这个Command类。这段代码会用namespace对象中的值来填充实例变量。
一旦对象配置完成,我们就可以通过调用run()方法来设置它并开始执行命令工作。这个类实现的是一个相对简单的用例。
main= SomeCommand()
main.config= config
main.run()
下面是一个实现了21点模拟操作的具体子类。
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 ):
dealer_rule= self.dealer_rule_mapself.dealer_rule
split_rule= self.split_rule_mapself.split_rule
try:
payout= ast.literal_eval( self.payout )
assert len(payout) == 2
except Exception as e:
raise Exception( "Invalid payout {0}".format(self.payout) )
from e
table= Table( decks=self.decks, limit=self.limit,
dealer=dealer_rule,
split=split_rule, payout=payout )
player_rule= self.player_rule_mapself.player_rule
betting_rule= self.betting_rule_mapself.betting_rule
player= Player( play=player_rule, betting=betting_rule,
rounds=self.rounds, stake=self.stake )
simulate= Simulate( table, player, self.samples )
with open(self.outputfile, "w", newline="") as target:
wtr= csv.writer( target )
wtr.writerows( simulate )
这个类实现了基本的顶层函数,这个函数用于配置不同的对象然后执行模拟操作。我们将前面介绍的simulate _ blackjack()函数封装起来创建了Command类的一个具体的扩展类。这可以像下面的代码这样将其用在主脚本中。
if name == "main":
with Logging_Config():
with Application_Config() as config:
main= Simulate_Command()
main.config= config
main.run()
尽管我们可以让这个命令成为Callable并且用main()代替main.run(),但是,这里使用可调用对象可能会引起混乱。我们显式地分离了以下3个设计问题。
- 构造:特意保留初始化为空。在后面的部分中,我们会向你介绍一些PITL的例子,在这些例子中,我们会从一些很小的组件命令创建出一个更大的复合命令。
- 配置:通过property设置器将配置导入,这样就与创建和控制的代码分离了。
- 控制:在构造和配置完成后,这是真正执行命令定义的操作部分。
当我们介绍可回调对象和函数时,构造是定义的一部分。配置和控制被合并为函数调用本身的一部分。如果我们想要定义可回调对象,就必须牺牲一部分灵活性。
16.6.2 添加用于分析的命令子类
我们会扩展应用程序,添加分析功能。由于我们在使用命令设计模式,因此可以为分析功能再添加另一个子类。
下面是我们的分析功能。
class Analyze_Command( Command ):
def run( self ):
with open(self.outputfile, "r", newline="") as target:
rdr= csv.reader( target )
outcomes= ( float(row[10]) for row in rdr )
first= next(outcomes)
sum_0, sum_1 = 1, first
value_min = value_max = first
for value in outcomes:
sum_0 += 1 # value0
sum_1 += value # value1
value_min= min( value_min, value )
value_max= max( value_max, value )
mean= sum_1/sum_0
print(
"{4}\nMean = {0:.1f}\nHouse Edge = {1:.1%}\nRange = {2:.1f}
{3:.1f}".format(
mean, 1-mean/50, value_min, value_max, self.outputfile) )
从统计学的角度来看,这段代码意义不大。但是,这里的关键是向你展示用第2个使用配置命名空间的命令完成和我们的模拟相关的工作。我们用了outputfile配置参数命名来执行一些统计分析的文件。
16.6.3 向应用程序中添加更多的功能
前面,我们介绍了一种支持多个功能的通用方法。一些应用程序中使用了多个顶层的main程序,它们分别位于独立的.py脚本文件中。
当我们想要合并不同文件中的命令时,我们必须编写一个用于创建更高层次的复合程序的shell脚本。再引入另一个工具和另一种语言来做PITL看起来不是一个好主意。
一个更灵活一些的方法是创建独立的脚本文件,然后基于位置参数来选择某个顶层Command对象。在我们的例子中,我们想要选择模拟或者分析命令。为此,可以在命令行参数中添加一个参数以解析下面的代码。
parser.add_argument( "command", action="store", default='simulate',
choices=['simulate', 'analyze'] )
parser.add_argument( "outputfile", action="store", metavar="output" )
这段代码会更改命令行API并将顶层动词添加到命令行中。我们可以很容易地将参数映射到对应的类名。
{'simulate': Simulate_Command, 'analyze': Analyze_Command}[options.
command]
这允许我们创建更高级的复合功能。例如,我们可能想要将模拟和分析合并到一个全局的程序中。同时,也希望可以不用shell就能够实现这点。
16.6.4 设计更高级的复合命令
下面我们会介绍如何基于一些命令设计一个复合命令。我们用两种设计策略:复合对象和复合类。
如果我们使用复合对象,那么复合命令就是基于内置的list或者tuple。我们可以扩展或封装某个现有的序列。我们会创建复合的Command对象作为保存其他Command对象的集合。我们可能会考虑一些类似下面这样的代码。
simulate_and_analyze = [Simulate(), Analyze()]
这样做的缺点是我们还没有为独特的复合命令创建新类。我们创建了一个通用的复合对象,并且用实例填充它。如果想要创建更高级的复合对象,就必须基于内置的序列类解决底层的Command类和更高层的复合Command对象间不对称的问题。
我们倾向于把一个复合命令也当作命令的一个子类。如果使用复合类,那么对于底层命令和更高层的复合命令,我们会获得一个更一致的结构。
下面是一个实现了一系列其他命令的类。
class CommandSequence(Command):
sequence = []
def init( self ):
self._sequence = [ class() for class_ in self.sequence ]
def set_config( self, config ):
for step in self._sequence:
step.config= config
config= property( fset=set_config )
def run( self ):
for step in self._sequence:
step.run()
我们定义了一个类级变量——sequence,它包含一系列的命令类。当对象初始化时,init()会用self.sequence中的命名类对象构造一个内部的实例变量——_sequence。
当配置完成后,它会被推送给每个复合对象。当复合命令通过run()执行时,该操作会被委托给复合命令中的每个组件来完成。
下面是基于两个Command子类创建的另一个Command子类。
class Simulate_and_Analyze(Command_Sequence):
sequence = [Simulate_Command, Analyze_Command]
现在,我们可以创建一个包含一系列独立步骤的类。由于这是Command类的一个子类,它包含必要的多态性API。现在,可以用这个类创建复合命令,因为它与其他Command的子类兼容。
现在,我们对参数解析过程做一个很小的修改就可以将这个功能添加到应用程序中。
parser.add_argument( "command", action="store", default='simulate',
choices=['simulate', 'analyze', 'simulate_analyze'] )
我们简单地将另外一个选择添加到参数选项中,还需要修改参数选项字符串和类之间的映射。
{'simulate': SimulateCommand, 'analyze': Analyze_Command, 'simulate
analyze': Simulate_and_Analyze}[options.command]
请注意,我们不应该用类似于both这样模糊的名称来合并两个命令。如果程序中没有这种模糊的概念,我们就为扩展和修改应用程序保留了可能。用命令设计模式让添加功能变得很容易。我们可以定义复合命令或者将一个很大命令分解为一些更小的命令。
打包和实现可能包括添加一个选项并将该选项映射到一个类名。如果我们使用更完整的配置文件(参见第13章“配置文件和持久化”),就可以直接在配置文件中提供类名并且保存从选项字符串到类的映射。
