💎一站式轻松地调用各大LLM模型接口,支持GPT4、智谱、星火、月之暗面及文生图 广告
[TOC] 在本章中,我们将介绍更多的设计模式。我们将再次涵盖典型的例子以及任何常见的Python替代实现方案。我们将讨论: * 适配器模式 * 门面模式 * 惰性初始化和享元模式 * 命令模式 * 抽象工厂模式 * 组合模式 ## 适配器模式 与我们在第8章“字符串和序列化”中回顾的大多数模式不同,适配器模式旨在与已存在的代码进行交互。我们不会设计一组全新的实现适配器模式的对象。适配器模式允许两个预先存在的对象一起工作,即使它们的接口并不兼容。像允许VGA投影仪插入HDMI端口的显示适配器(译注:有些投影仪没有HDMI接口,需要使用HDMI转换器把HDMI信号转换为VGA或者DVI信号再接入投影仪才能使用,见百度),适配器对象位于两个不同的接口之间,转换他们之间的信号。适配器对象的唯一目的是执行此翻译工作。适应可能的多种任务,例如将参数转换为不同的格式,重新排列参数的顺序,调用不同名称的方法,或者提供默认参数。 </b> 在结构上,适配器模式类似于简化的装饰器模式。装饰器通常为它们所替换的接口提供相同的接口,而适配器映射在两种不同的接口。下面是适配器模式的UML: ![](https://box.kancloud.cn/90d2a22ea508a7a2ebbbff60bc8b1b29_394x229.png) 在这里,接口1期望调用一个名为`make_action(some, arguments)`的方法。我们已经有了这个完美的接口2类,可以做我们想做的任何事情(为了避免重复,我们不想重写它!),但它提供了一个不同的名为`different_action(other, arguments)`的方法。适配器类实现了`make_action`接口并将参数映射到现有接口。 </b> 这种模式的优点是从一个接口映射到另一个接口的代码都在一个地方。另一种选择则非常丑陋;无论何时我们需要访问这些代码,我们都必须在多个地方翻译这些代码。 </b> 例如,假设我们有以下预先存在的类,它采用一个字符串日期格式为“YYYY-MM-DD”,并计算一个人当天的年龄: ``` class AgeCalculator: def __init__(self, birthday): self.year, self.month, self.day = ( int(x) for x in birthday.split('-')) def calculate_age(self, date): year, month, day = ( int(x) for x in date.split('-')) age = year - self.year if (month,day) < (self.month,self.day): age -= 1 return age ``` 这是一个非常简单的类,做它应该做的事情。但是我们得搞清楚程序员在想什么,为什么用一个特别格式化的字符串而不是使用Python非常有用的内置`datetime`库。作为有良心的、尽可能重用代码的程序员,我们编写的大多数程序都会与`datetime`对象交互,而不是字符串。 </b> 我们有几种选择来解决这种情况;我们可以重写这个类接受`datetime`对象,这可能更准确。但是如果这个类是由第三方提供的,我们不知道或者不能改变它的内部结构,我们需要尝试其他办法。我们可以按原样使用这个类,并且每当我们想要计算`datetime.date`对象的年龄时,我们可以调用`datetime.date.strftime('%Y-%m-%d')`将其转换为正确的格式。但是转换会在很多地方发生,更糟糕的是,如果我们错误的将`%m`写成`%M`,它将给出当前分钟,而不是输入的月份!想像 如果你在十几个不同的地方写了这些方法,当你意识到你的错误,你只能回去改变它。这是不可维护的代码,它打破了DRY原则。 </b> 相反,我们可以编写一个适配器,允许将正常日期插入普通`AgeCalculator`类: ``` import datetime class DateAgeAdapter: def _str_date(self, date): return date.strftime("%Y-%m-%d") def __init__(self, birthday): birthday = self._str_date(birthday) self.calculator = AgeCalculator(birthday) def get_age(self, date): date = self._str_date(date) return self.calculator.calculate_age(date) ``` 此适配器转换`datetime.date`和`datetime.time`(它们具有相同的`strftime`接口)转换成我们原来`AgeCalculator`可以使用的字符串。现在我们可以在新接口上使用原始代码。我改变了方法签名为`get_age`,来展示调用接口可能也在寻找不同的方法名,而不仅仅是不同类型的参数。 </b> 创建一个类作为适配器是实现这种模式的常用方法,但是,通常在Python中还有其他方法可以做到这一点。继承和多重继承可用于向类添加功能。例如,我们可以添加一个适配器以便与原始`AgeCalculator`类一起工作: ``` import datetime class AgeableDate(datetime.date): def split(self, char): return self.year, self.month, self.day ``` 正是这样的代码让人怀疑Python是否合法。我们向我们的子类添加了一个`split`方法,它只接受一个参数(我们忽略了这个参数),并返回年、月和日的元组。这和原始`AgeCalculator`类一起使用是违法的(译注:中文书将`lawlessly`翻译成完美的),因为代码在特殊格式的字符串上调用`strip`,在这种情况下,`strip`返回年、月和日元组。`AgeCalculator`代码只关心`strip`是否存在,并返回可接受的值;它并不关心我们是否真的以字符串形式传递。它真的有用: ``` >>> bd = AgeableDate(1975, 6, 14) >>> today = AgeableDate.today() >>> today AgeableDate(2015, 8, 4) >>> a = AgeCalculator(bd) >>> a.calculate_age(today) 40 ``` 这行得通,但这是个愚蠢的想法。在这个特定的例子中,我们很难维护这样的适配器。我们很快就会忘记为什么我们需要在`date`类中添加一个`strip`方法。方法名称不明确。这可能是适配器的本质,但是请显式地创建适配器,而不是像往常那样使用继承阐明了它的目的。 </b> 除了继承,我们有时也可以使用猴子修补给现有的类添加方法。这不适用于`datetime`对象,因为它不允许在运行时添加属性,但是在普通类中,我们可以添加一个新方法,它提供了调用代码所需的自适应接口。或者,我们可以扩展或猴子补丁`AgeCalculator`本身来替换`calculate_age`方法,这样更符合我们的需求。 </b> 最后,通常可以将函数用作适配器;这显然不符合适配器模式的实际设计,但是如果我们回想一下,函数本质上是带有一个`__call__`方法的对象,它就变成了一种显而易见的适配器。 ## 门面模式 门面模式旨在为复杂组件系统提供简单接口。对于复杂的任务,我们可能需要直接与这些对象交互,但是对于这些复杂的系统,通常有一个“典型”的用法,互动不是必要的。门面模式允许我们定义一个新的对象封装系统的这种典型用法。任何时候我们想访问一般功能,我们可以使用单个对象的简化接口。如果项目的另一部分需要访问更复杂的功能,它仍然能够与系统直接交互。门面模式的UML图实际上依赖于 子系统,但是以一种模糊的方式,看起来是这样的: ![](https://box.kancloud.cn/3e728aff08caa57c86c9c46fa3a74bdf_310x278.png) 门面模式在很多方面都像适配器。主要的区别是门面模式试图从复杂的接口中抽象出一个更简单的接口,而适配器仅试图将一个现有接口映射到另一个接口。 </b> 让我们为电子邮件应用程序写一个简单的门面模式。一个用Python发送电子邮件的低级库,正如我们在第7章“面向对象的快捷方式”中看到的,相当复杂。两个接收消息的库甚至更糟。 </b> 如果有一个简单的类,允许我们发送一封电子邮件,列出当前在IMAP或POP3连接上收件箱中的电子邮件,会比较好。为了保持我们的例子的简洁,我们将继续使用IMAP和SMTP:两个完全不同的、碰巧处理电子邮件的子系统。我们的门面模式只执行两个任务:发送一封电子邮件到特定的地址,并通过IMAP连接检查收件箱。关于连接,我们做一些常见假设,例如两个SMTP和IMAP的主机位于同一个地址,两者的用户名和密码相同,都使用标准端口。这涵盖了许多电子邮件服务器的情况,但是如果程序员需要更多的灵活性,他们总是可以绕过门面模式直接访问两个子系统。 </b> 该类用电子邮件服务器的主机名、登录用户名和密码进行初始化: ``` import smtplib import imaplib class EmailFacade: def __init__(self, host, username, password): self.host = host self.username = username self.password = password ``` `send_email`方法格式化电子邮件地址和消息,使用`smtplib`发送邮件。这不是一项复杂的任务,但需要一点时间将“自然”输入参数传递到门面模式,以获得smtplib发送消息的正确格式: ``` def send_email(self, to_email, subject, message): if not "@" in self.username: from_email = "{0}@{1}".format( self.username, self.host) else: from_email = self.username message = ("From: {0}\r\n" "To: {1}\r\n" "Subject: {2}\r\n\r\n{3}").format( from_email, to_email, subject, message) smtp = smtplib.SMTP(self.host) smtp.login(self.username, self.password) smtp.sendmail(from_email, [to_email], message) ``` 方法开头的`if`语句正在捕捉用户名是否是完整的“发件人”电子邮件地址,或者只是@符号左侧的部分;不同的主机对登录细节的处理不同。 </b> 最后,获取收件箱中当前邮件的代码非常混乱;IMAP协议被痛苦地过度设计,imaplib标准库是协议上只有一个薄层: ``` def get_inbox(self): mailbox = imaplib.IMAP4(self.host) mailbox.login(bytes(self.username, 'utf8'), bytes(self.password, 'utf8')) mailbox.select() x, data = mailbox.search(None, 'ALL') messages = [] for num in data[0].split(): x, message = mailbox.fetch(num, '(RFC822)') messages.append(message[0][1]) return messages ``` 现在,如果我们把所有这些加在一起,我们有一个简单的`facade`类,它可以以一种相当直接的方式发送和接收信息,比我们直接与这些复杂的库交互要简单得多。 </b> 虽然它在Python社区中很少被提及,但是门面模式是Python生态系统不可分割的一部分。因为Python强调语言可读性,语言及其库都倾向于为复杂任务提供易于理解的接口。例如,对于循环而言,列表解析和生成器都是更复杂迭代器协议的门面。`defaultdict`实现是一个抽象出来的门面,它为字典里没有的键提供默认值。第三方`requests `库是针对HTTP查询不太可读的程序库上的一个强大门面。 ## 享元模式 享元模式是一种内存优化模式。初级Python程序员倾向于忽略内存优化,假设内置垃圾收集器将好好照顾他们。这通常是完全可以接受的,但是当发展得更大时具有许多相关对象的应用程序,关注内存问题可以有巨大的回报。 </b> 享元模式基本上确保共享状态的对象们可以使用共享状态的相同内存。它通常只在一个程序之后出现内存问题时才会使用。在某些情况下从一开始就进行优化配置设计是有意义的,但请记住过早优化是创建程序的最有效方法,但维护起来也很复杂。 </b> 让我们看一下享元模式的UML: ![](https://box.kancloud.cn/889c8e6438e1f7cc42c7ce1e502dcc7c_518x201.png) 每个`Flyweight`都没有特定的状态;当它需要在`SpecicState`执行一个操作,该状态需要通过调用代码传递给`Flyweight`。传统上,返回`flyweight`的工厂是一个单独的对象;它的目的是返回一个给定键的`flyweight`,该键用于标识这个`flyweight`。它的工作原理就像第10章“Python设计模式I”中讨论的单例模式;如果`flyweight`存在,我们就返回它;否则,我们会创建一个新的`flyweight`。在许多语言中,工厂是不是作为单独的对象,而是作为`Flyweight`类上的静态方法。 </b> 想想汽车销售的库存系统。每辆车都有特定的序列号和特定的颜色。但是特定型号的所有汽车的大部分细节都是一样的。例如,本田飞度DX车型就是一个简单的、特色不多的例子。LX车型有空调、方向盘调节、巡航、电动车窗和锁。这款运动车型有别致的轮子、一个USB充电器和一个扰流板。没有享元模式下,每个单独的汽车对象必须存储一长串它有和没有的特性。考虑到本田在美国一年销售的汽车数量, 这将会浪费大量的内存。使用享元模式中,我们可以用共享对象替代与车型相关的特征列表,之后只需对每台车简单地引用该共享对象,以及序列号和颜色。在Python中,享元工厂通常使用那个时髦的`__new__`构造函数,类似于我们对单例模式中所做的。不同于单例模式只需要返回类的一个实例,我们需要能够根据键返回不同的实例。我们可以将项目储存在字典里,根据键进行查找。这个解决方案是有问题的,但是,因为该项目只要在字典中,就会一直保留在内存中。如果我们已经卖完了LX飞度车型,就不再需要了享元了,但它仍然存在于字典里。我们当然可以在卖车的时候清理干净,但是垃圾收集器不就是为了这个吗? </b> 我们可以利用Python的`weakref`模块来解决这个问题。这个模块 提供了一个`WeakValueDictionary`对象,它基本上允许我们将项目存储在字典里,而不用关心垃圾收集。如果值在弱引用字典,在应用程序中的任何地方,没有对该对象的其他引用(也就是说,我们卖完了LX型号),垃圾回收器最终会为我们收拾残局。 </b> 让我们先为我们的享元建造工厂: ``` import weakref class CarModel: _models = weakref.WeakValueDictionary() def __new__(cls, model_name, *args, **kwargs): model = cls._models.get(model_name) if not model: model = super().__new__(cls) cls._models[model_name] = model return model ``` 基本上,每当我们用一个给定的名字构造一个新的享元,我们首先在弱引用字典中查找该名称;如果它存在,我们返回那个车型;如果不存在,我们创建一个新的车型。不管怎样,我们都知道享元的`__init__`每次都会被调用,不管它是新的还是现有的对象。因此,我们的`__init__`方法可以如下所示: ``` def __init__(self, model_name, air=False, tilt=False, cruise_control=False, power_locks=False, alloy_wheels=False, usb_charger=False): if not hasattr(self, "initted"): self.model_name = model_name self.air = air self.tilt = tilt self.cruise_control = cruise_control self.power_locks = power_locks self.alloy_wheels = alloy_wheels self.usb_charger = usb_charger self.initted=True ``` `if`语句确保我们仅在第一次调用`__init__`方法才对对象进行初始化。这意味着我们可以稍后用车型名调用工厂,取回同一个享元对象。然而,因为如果没有外部引用,享元将被垃圾回收,我们必须小心不要意外使用空值创建新的享元。 </b> 让我们给我们的享元添加一个方法,假设查找指定车型的车辆序列号(译注:就是车身号,和身份证类似),并确定是否涉及任何事故。 这种方法需要访问汽车的序列号,序列号因车而异;它不能与享元一起存储。因此,这些数据必须通过调用代码传递到方法中: ``` def check_serial(self, serial_number): print("Sorry, we are unable to check " "the serial number {0} on the {1} " "at this time".format( serial_number, self.model_name)) ``` 我们可以定义存储额外信息的类,同时包括一个对享元的引用: ``` class Car: def __init__(self, model, color, serial): self.model = model self.color = color self.serial = serial def check_serial(self): return self.model.check_serial(self.serial) ``` 我们可以追踪可用的车型和库存车辆: ``` >>> dx = CarModel("FIT DX") >>> lx = CarModel("FIT LX", air=True, cruise_control=True, ... power_locks=True, tilt=True) >>> car1 = Car(dx, "blue", "12345") >>> car2 = Car(dx, "black", "12346") >>> car3 = Car(lx, "red", "12347") ``` 让我们展示弱引用是如何工作的: ``` >>> id(lx) 3071620300 >>> del lx >>> del car3 >>> import gc >>> gc.collect() 0 >>> lx = CarModel("FIT LX", air=True, cruise_control=True, ... power_locks=True, tilt=True) >>> id(lx) 3071576140 >>> lx = CarModel("FIT LX") >>> id(lx) 3071576140 >>> lx.air True ``` `id`函数告诉我们对象的唯一标识符。当我们删除对LX车型的所有引用并强制垃圾收集之后,再一次调用这个函数,我们发现ID已经变化了。`CarModel __new__ `工厂字典中的值被删除了,并创建了一个新值。如果我们尝试构建第二个车型实例中,它返回相同的对象(ID相同),即使我们在第二次调用中没有提供任何参数,`air`变量仍然设置为`True`。这意味着对象没有被第二次初始化,这正是我们的设计初衷。 </b> 显然,使用享元模式可比仅存储单一车型特征更复杂。我们应该什么时候选择使用它?享元模式专为节省内存而设计;如果我们有几十万个类似的对象,将相似的属性组合成享元会对内存消耗产生巨大影响 。对于优化的编程解决方案来说,这种很常见的优化CPU、内存或磁盘空间的解决方案,与未优化的代码相比,会导致更复杂的代码。因此,在做出以下决定时,权衡代码可维护性和优化的利弊是非常重要的。选择优化时,请尝试使用享元模式这样的模式,以确保优化引入的复杂性被锁在为代码的单个(有良好文档的)部分。 ## 命令模式 命令模式在必须完成的操作之间添加了一个抽象级别,通常在稍后,调用这些操作的对象。在命令模式下,客户端代码创建一个可以在以后执行的`command`对象。该对象知道当它在一个接收器对象上执行时,接收器对象将管理其自身内部状态。`command`对象实现一个特定的接口(通常它有一个`execute`或`do_action`方法,并跟踪任何执行操作所需的参数)。最后,一个或多个`Invoker`对象在正确的时间执行命令。 </b> UML如下图: ![](https://box.kancloud.cn/57f7c40b1249783039e22529c5d35fdb_346x239.png) 命令模式的一个常见示例是图形窗口上的操作。通常,可以通过菜单栏上的菜单项,键盘快捷方式、工具栏图标或上下文菜单调用一个操作。这些都是`Invoker`对象的例子。实际操作发生时,如退出、保存或复制,都是`CommandInterface`的实现。接收退出的GUI窗口,接收保存的文档,和接收复制命令的贴板管理器,都是可能`Receivers`的例子。 </b> 让我们实现一个简单的命令模式,它为`Save`和`Exit`操作提供。我们将从一些普通的接收器类开始: ``` import sys class Window: def exit(self): sys.exit(0) class Document: def __init__(self, filename): self.filename = filename self.contents = "This file cannot be modified" def save(self): with open(self.filename, 'w') as file: file.write(self.contents) ``` 这些模拟类为对象建模,这些对象可能在工作环境中做更多事情。窗口需要处理鼠标移动和键盘事件,文档需要处理字符插入、删除和 选择。但在我们的例子,这两个类只做我们需要的。 </b> 现在让我们定义一些调用类。这些将对工具栏、菜单和可能发生的键盘事件进行建模;再说一次,它们实际并没有连接任何东西,但是我们可以看到它们是如何与命令、接收器和客户端代码分离的: ``` class ToolbarButton: def __init__(self, name, iconname): self.name = name self.iconname = iconname def click(self): self.command.execute() class MenuItem: def __init__(self, menu_name, menuitem_name): self.menu = menu_name self.item = menuitem_name def click(self): self.command.execute() class KeyboardShortcut: def __init__(self, key, modifier): self.key = key self.modifier = modifier def keypress(self): self.command.execute() ``` 请注意各种操作方法是如何在各自的命令中调用`execute`方法的?此代码并没有显示`command`属性被设置在每个对象上。它们可以被传递到`__init__`函数中,但是因为它们可能被更改(例如,用一个可定制的键绑定编辑器),所以稍后将属性设置在对象可能要更有意义。 </b> 现在,让我们连接命令本身: ``` class SaveCommand: def __init__(self, document): self.document = document def execute(self): self.document.save() class ExitCommand: def __init__(self, window): self.window = window def execute(self): self.window.exit() ``` 这些命令很简单;他们展示了基本的模式,但确实需要注意的是,如有必要,我们可以用命令存储状态和其他信息。例如,如果我们有一个插入字符的命令,我们可以保持当前插入字符的状态。 </b> 现在我们所要做的就是连接一些客户端和测试代码来让命令模式发挥作用。对于基本测试,我们可以在脚本的末尾包括这些代码: ``` window = Window() document = Document("a_document.txt") save = SaveCommand(document) exit = ExitCommand(window) save_button = ToolbarButton('save', 'save.png') save_button.command = save save_keystroke = KeyboardShortcut("s", "ctrl") save_keystroke.command = save exit_menu = MenuItem("File", "Exit") exit_menu.command = exit ``` 首先,我们创建两个接收器对象和两个命令对象。然后我们创建了几个可用的调用程序,并在每个调用程序上设置正确的命令属性。为了测试,我们可以使用`python3 -i filename.py`并运行类似`exit_menu.click()`的代码,这将结束程序,或者`save _ keystroke.keystroke()`,它将保存假文件。 </b> 不幸的是,前面的例子并不让人觉得很Python。它们有许多“样板代码”(不完成任何事情,只提供结构)和`command`类都非常相似。也许我们可以创建一个通用的命令对象作为回调函数? </b> 事实上,为什么要费心呢?我们可以为每个命令对象使用一个函数或方法对象吗?除了使用`execute()`方法的对象,我们还可以编写函数,并将它直接用作命令。这是Python中命令模式的常见范例: ``` import sys class Window: def exit(self): sys.exit(0) class MenuItem: def click(self): self.command() window = Window() menu_item = MenuItem() menu_item.command = window.exit ``` 这看起来更像Python。乍一看,我们好像已经移除了命令模式,我们已经紧密地连接了`menu_item`和`Window`类。但是如果我们仔细观察,我们发现根本没有紧密耦合。任何可调用的对象都可以像以前一样被设置为`menu_item`的命令属性。`Window.exit`方法可以附加到任何调用程序上。命令模式的大部分灵活性仍然保持着。为了可读性,我们牺牲了完全解耦,但是在我和许多Python程序员看来,与完全抽象的版本比较,这种写法更容易维护。 </b> 当然,因为我们可以向任何对象添加`__call__`方法,所以我们不受限于函数。当调用不需要维护状态时,前面的示例是一个有用的快捷方式,但是在更高级的用法中,我们可以使用下面的代码: ``` class Document: def __init__(self, filename): self.filename = filename self.contents = "This file cannot be modified" def save(self): with open(self.filename, 'w') as file: file.write(self.contents) class KeyboardShortcut: def keypress(self): self.command() class SaveCommand: def __init__(self, document): self.document = document def __call__(self): self.document.save() document = Document("a_file.txt") shortcut = KeyboardShortcut() save_command = SaveCommand(document) shortcut.command = save_command ``` 这里我们有一些看起来像第一个命令模式的东西,但更多是一些惯用的。如你所见,让调用方调用可调用对象,而不是带有`execute`方法的`command`对象,这没有以任何方式限制我们。事实上,它给了我们更多的灵活性。当它工作时,我们可以直接链接到函数,但是当情况需要时,我们可以构建一个完整的可调用`command`对象。 </b> 命令模式经常被扩展以支持不可撤销的命令。例如,文本程序可以将每次插入包装在单独的命令中,除了带有`execute`方法,但也可以使用`undo`方法来删除插入。图形程序可以包装每个绘图动作(矩形、线条、手绘像素等)在一个具有`undo`方法的`command`对象中,该方法将像素重置为其原始状态。在这种情况下,命令模式的解耦显然更有用,因为每个操作都必须保持足够长时间的状态,才能在稍后执行`undo`方法。 ## 抽象的工厂模式 当我们有多种依赖于某些配置或平台问题的系统实现可能时,通常使用抽象工厂模式。调用代码从抽象工厂请求一个对象,但并不确切知道将返回什么类型的对象。返回对象的底层实现可能取决于多种因素,例如当前的语言、操作系统或本地配置。 </b> 抽象工厂模式的常见示例包括独立于操作系统的工具包、数据库后端和特定国家格式化程序或计算器。独立于操作系统的GUI工具包可能使用抽象工厂模式,Windows系统将返回一组WinForm小部件,Mac系统将返回Cocoa小部件,Gnome将返回GTK小部件,KDE将返回QT小部件。Django提供一个抽象工厂,该工厂根据当前站点的配置设置,返回一组与特定数据库后端(MySQL、PostgreSQL、SQLite等)进行交互的对象关系类。如果需要在多个地方部署应用程序,每一个都可以通过只改变配置变量来使用不同的数据库后端。不同国家对零售商品有不同的税收计算、小计和总计系统;抽象工厂可以返回特定的税收计算对象。 </b> 没有具体的例子,很难理解抽象工厂模式的UML类图,让我们先扭转局面,创造一个具体的例子。我们将创建一组依赖于特定语言环境的格式化程序,帮助我们格式化日期和货币。我们将会有一个抽象的工厂类,用来选择特定的工厂,包括几个具体的工厂,一个在法国,一个在美国。其中每一个都将为日期和时间创建格式化程序对象,它可以特定的格式化值被查询。下面是UML: ![](https://box.kancloud.cn/c2eb7fcd9fe99b35511e91633453b78f_798x424.png) 与早期更简单的文本进行比较可以发现,这张图并不总是价值千金,特别是考虑到我们甚至还没有考虑工厂选择代码。 </b> 当然,在Python中,我们不需要实现任何接口类,所以我们可以放弃`DateFormatter`、`CurrencyFormatter`和`FormatterFactory`。 格式化类本身非常简单,详细说明如下: ``` class FranceDateFormatter: def format_date(self, y, m, d): y, m, d = (str(x) for x in (y,m,d)) y = '20' + y if len(y) == 2 else y m = '0' + m if len(m) == 1 else m d = '0' + d if len(d) == 1 else d return("{0}/{1}/{2}".format(d,m,y)) class USADateFormatter: def format_date(self, y, m, d): y, m, d = (str(x) for x in (y,m,d)) y = '20' + y if len(y) == 2 else y m = '0' + m if len(m) == 1 else m d = '0' + d if len(d) == 1 else d return("{0}-{1}-{2}".format(m,d,y)) class FranceCurrencyFormatter: def format_currency(self, base, cents): base, cents = (str(x) for x in (base, cents)) if len(cents) == 0: cents = '00' elif len(cents) == 1: cents = '0' + cents digits = [] for i,c in enumerate(reversed(base)): if i and not i % 3: digits.append(' ') digits.append(c) base = ''.join(reversed(digits)) return "{0}€{1}".format(base, cents) class USACurrencyFormatter: def format_currency(self, base, cents): base, cents = (str(x) for x in (base, cents)) if len(cents) == 0: cents = '00' elif len(cents) == 1: cents = '0' + cents digits = [] for i,c in enumerate(reversed(base)): if i and not i % 3: digits.append(',') digits.append(c) base = ''.join(reversed(digits)) return "${0}.{1}".format(base, cents) ``` 这些类使用一些基本的字符串操作,尝试转换各种可能的输入(整数、不同长度的字符串等),并将它们转换为以下格式: | | 美国 |法国 | | --- | --- | --- | | 日期格式| mm-dd-yyyy | dd/mm/yyyy | | 货币格式| $14,500.50 | 14 500€50 | 显然,这段代码中的输入可能需要更多的验证,但是让我们尽可能让这个例子既简单又愚蠢。 </b> 既然已经设置了格式化程序,我们只需要创建格式化程序工厂: ``` class USAFormatterFactory: def create_date_formatter(self): return USADateFormatter() def create_currency_formatter(self): return USACurrencyFormatter() class FranceFormatterFactory: def create_date_formatter(self): return FranceDateFormatter() def create_currency_formatter(self): return FranceCurrencyFormatter() ``` 现在,我们设置选择适当格式化程序的代码。因为这是那种只需要设置一次的东西,我们可以把它变成一个单例——虽然单例在Python中不是很有用。让我们将当前格式化程序设为改为模块级变量: ``` country_code = "US" factory_map = { "US": USAFormatterFactory, "FR": FranceFormatterFactory} formatter_factory = factory_map.get(country_code)() ``` 在这个例子中,我们硬编码当前的国家代码;实际上,很可能反思语言环境、操作系统或配置文件来选择代码。本示例使用字典将国家代码与工厂类别相关联。然后我们从字典中获取正确的类并实例化它。 </b> 当我们想要增加对更多国家的支持时,很容易看到需要做什么:创建新的格式化程序类和抽象工厂本身。记住`Formatter`类可能被重用;例如,加拿大将其货币格式化和美国一样,但是它的日期格式比它的南方邻居更合理。 </b> 抽象工厂经常返回单例对象,但这不是必需的;在我们的代码中, 每次调用它时,它都会返回每个格式化程序的新实例。没有理由格式化程序无法存储为实例变量,并且为每个工厂返回了同一个实例。 </b> 回顾这些例子,我们再次看到,似乎有很多工厂的样板代码,在Python中,没有必要出现。通常情况下,对每种工厂类型使用单独的模块(例如:美国和法国),使得调用抽象工厂的需求更容易得到满足,然后确保在工厂模块中访问正确的模块。此类模块的包结构可能如下所示: ``` localize/ __init__.py backends/ __init__.py USA.py France.py … ``` 诀窍是本地化包中的`__init__.py`,它可以包含所有对正确后端的请求重定向的逻辑。有多种方法可以做到这一点。 </b> 如果我们知道后端永远不会动态变化(也就是说,没有重启),我们可以在`__init__.py`中放一些`if`语句。检查当前的国家代码,并使用通常不可接受的`from .backends.USA import *`语法从适当的后端导入所有变量。或者,我们可以导入每个后端并将`current _ backend`变量设置为指向特定模块: ``` from .backends import USA, France if country_code == "US": current_backend = USA ``` 根据我们选择的解决方案,我们的客户端代码必须为当前国家的语言环境调用`localize.format_date`或`localize.current_back.format_date`获取格式化日期。最终的结果比原始的抽象工厂模式更加Python,更加典型的用法,也更灵活。 ## 组合模式 组合模式允许通过简单组件构建复杂的树状结构。这些被称为组合对象的组件有点像容器或变量,取决于它们是否有子组件。组合对象是容器对象,其中内容实际上可能是另一个组合对象。 </b> 传统上,组合对象中的每个组件必须是叶节点(即不能包含其他对象)或组合节点。关键是两者都是组合的,叶节点可以具有相同的接口。UML图非常简单: ![](https://box.kancloud.cn/9960fea529292c7f31d7c3cc680e64ad_328x179.png) 然而,这个简单的模式允许我们创建复杂的元素安排,所有这些都满足组件对象的接口。这里有一个有关复杂安排的具体例子: ![](https://box.kancloud.cn/96dd94e775aad381a0ec922a3126711b_486x245.png) 组合模式通常在类似文件/文件夹的树中很有用。无论树中的节点是普通文件还是文件夹,它仍然会受到操作的影响,例如移动、复制或删除节点。我们可以创建一个支持这些操作的组件接口,然后使用组合对象表示文件夹,用叶节点来表示正常文件。 </b> 当然,在Python中,我们可以再次利用鸭子类型来隐式地提供接口,所以我们只需要编写两个类。让我们先定义这些接口: ``` class Folder: def __init__(self, name): self.name = name self.children = {} def add_child(self, child): pass def move(self, new_path): pass def copy(self, new_path): pass def delete(self): pass class File: def __init__(self, name, contents): self.name = name self.contents = contents def move(self, new_path): pass def copy(self, new_path): pass def delete(self): pass ``` 对于每个文件夹(组合)对象,我们维护一个子字典。通常,列表就足够了,但是在这种情况下,字典对于通过名称查找孩子节点是有用的。我们的路径将被指定为由/字符分隔的节点名,类似于Unix命令解释器中的路径。 </b> 考虑到所涉及的方法,我们可以看到,无论是文件节点还是文件夹节点,移动或删除节点,都以类似的方式运行。但是,复制时,必须对文件夹节点进行递归复制,而复制文件节点则是一个小操作。 </b> 为了利用类似的操作,我们可以提取一些常见的方法放到父类中。让我们使用被丢弃的组件接口,将其改为基类: ``` class Component: def __init__(self, name): self.name = name def move(self, new_path): new_folder =get_path(new_path) del self.parent.children[self.name] new_folder.children[self.name] = self self.parent = new_folder def delete(self): del self.parent.children[self.name] class Folder(Component): def __init__(self, name): super().__init__(name) self.children = {} def add_child(self, child): pass def copy(self, new_path): pass class File(Component): def __init__(self, name, contents): super().__init__(name) self.contents = contents def copy(self, new_path): pass root = Folder('') def get_path(path): ``` 我们已经在组件类上创建了`move`和`delete`方法。它们两个访问一个我们还没有设置的神秘父变量。`move`方法使用给定的模块级`get_path`函数,它从预先确定的根节点发现一个节点。所有文件都将被添加到该根节点或该节点的子节点。对于`move`方法,目标应该是当前存在的文件夹,否则我们将会得到一个错误。与技术书籍中的许多例子一样,错误处理都不幸地缺失了,它们本该帮助我们关注正在考虑的原则。 </b> 让我们先设置那个神秘的父变量;这发生在文件夹的`add_child`方法: ``` def add_child(self, child): child.parent = self self.children[child.name] = child ``` Well, that was easy enough. Let's see if our composite ile hierarchy is working properly: 嗯,这很容易。让我们看看我们的组合文件层次结构是否能正常工作: </b> **$ python3 -i 1261_09_18_add_child.py** ``` >>> folder1 = Folder('folder1') >>> folder2 = Folder('folder2') >>> root.add_child(folder1) >>> root.add_child(folder2) >>> folder11 = Folder('folder11') >>> folder1.add_child(folder11) >>> file111 = File('file111', 'contents') >>> folder11.add_child(file111) >>> file21 = File('file21', 'other contents') >>> folder2.add_child(file21) >>> folder2.children {'file21': <__main__.File object at 0xb7220a4c>} >>> folder2.move('/folder1/folder11') >>> folder11.children {'folder2': <__main__.Folder object at 0xb722080c>, 'file111': <__main__. File object at 0xb72209ec>} >>> file21.move('/folder1') >>> folder1.children {'file21': <__main__.File object at 0xb7220a4c>, 'folder11': <__main__. Folder object at 0xb722084c>} ``` 是的,我们可以创建文件夹,将文件夹添加到其他文件夹,将文件添加到文件夹,以及移动他们!在文件层次结构中,我们还能要求什么? </b> 好吧,我们可以要求实施复制,但是为了保留这些树,让我们把复制 作为练习。 </b> 组合模式对于各种树状结构非常有用,包括GUI小部件层次结构、文件层次结构、树集、图形和HTML DOM。如前面的例子所示,当根据传统实现实施将是一个很有用的模式。有时候,如果只是一棵浅树正在被创建,我们可以摆脱列表或字典的字典,并且不需要实现自定义组件、叶和组合类。其他时候,我们可以摆脱只实现一个组合类,将叶和组合对象视为一个单独的类。或者,Python的鸭子类型可以轻松地将其他对象添加到组合层次结构中,只要它们有正确的接口。 ## 摘要 在本章中,我们详细介绍了另外几个设计模式,涵盖了它们的规范描述以及在Python中实现它们的替代方法,这通常比传统的面向对象语言更加灵活和通用。适配器模式对于匹配接口很有用,而门面模式适合简化它们。享元是一种复杂的模式,只在内存优化情况下才是必需的。在Python中,命令模式通常更多使用第一类函数作为回调来恰当地实现。抽象工厂允许根据配置或系统信息进行实现和运行分离。复合模式普遍用于树状结构。 </b> 在下一章中,我们将讨论测试Python程序有多重要,以及如何去做。