[TOC]
在本章中,我們將介紹更多的設計模式。我們將再次涵蓋典型的例子以及任何常見的Python替代實現方案。我們將討論:
* 適配器模式
* 門面模式
* 惰性初始化和享元模式
* 命令模式
* 抽象工廠模式
* 組合模式
## 適配器模式
與我們在第8章“字符串和序列化”中回顧的大多數模式不同,適配器模式旨在與已存在的代碼進行交互。我們不會設計一組全新的實現適配器模式的對象。適配器模式允許兩個預先存在的對象一起工作,即使它們的接口并不兼容。像允許VGA投影儀插入HDMI端口的顯示適配器(譯注:有些投影儀沒有HDMI接口,需要使用HDMI轉換器把HDMI信號轉換為VGA或者DVI信號再接入投影儀才能使用,見百度),適配器對象位于兩個不同的接口之間,轉換他們之間的信號。適配器對象的唯一目的是執行此翻譯工作。適應可能的多種任務,例如將參數轉換為不同的格式,重新排列參數的順序,調用不同名稱的方法,或者提供默認參數。
</b>
在結構上,適配器模式類似于簡化的裝飾器模式。裝飾器通常為它們所替換的接口提供相同的接口,而適配器映射在兩種不同的接口。下面是適配器模式的UML:

在這里,接口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圖實際上依賴于
子系統,但是以一種模糊的方式,看起來是這樣的:

門面模式在很多方面都像適配器。主要的區別是門面模式試圖從復雜的接口中抽象出一個更簡單的接口,而適配器僅試圖將一個現有接口映射到另一個接口。
</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:

每個`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如下圖:

命令模式的一個常見示例是圖形窗口上的操作。通常,可以通過菜單欄上的菜單項,鍵盤快捷方式、工具欄圖標或上下文菜單調用一個操作。這些都是`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:

與早期更簡單的文本進行比較可以發現,這張圖并不總是價值千金,特別是考慮到我們甚至還沒有考慮工廠選擇代碼。
</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圖非常簡單:

然而,這個簡單的模式允許我們創建復雜的元素安排,所有這些都滿足組件對象的接口。這里有一個有關復雜安排的具體例子:

組合模式通常在類似文件/文件夾的樹中很有用。無論樹中的節點是普通文件還是文件夾,它仍然會受到操作的影響,例如移動、復制或刪除節點。我們可以創建一個支持這些操作的組件接口,然后使用組合對象表示文件夾,用葉節點來表示正常文件。
</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程序有多重要,以及如何去做。