[TOC]
在上一章中,我們簡要介紹了設計模式,并介紹了迭代器模式,一種非常有用和常見的模式,它已經被抽象到編程語言本身的核心。在本章中,我們將回顧其他一些常見模式,以及它們在Python中的實現方式。同迭代一樣,Python經常提供替代語法來更簡單解決問題。我們將涵蓋“傳統”設計和這些模式的Python版本。總之,我們將看到:
* 眾多特定模式
* Python中每個模式的規范實現
* 取代某些模式的Python語法
## 裝飾器模式
裝飾模式允許我們用其他對象“包裝”一個提供核心功能的對象,這些對象可以改變核心功能。任何使用裝飾對象的對象與裝飾對象交互的方式,就像沒有被裝飾一樣(就是說,被裝飾對象的接口仍然與核心對象的接口是一樣的)。
</b>
裝飾模式有兩個主要用途:
* 增強組件向第二個組件發送數據時的響應內容
* 支持多種可選行為
第二種選擇通常是多重繼承合適的替代方案。我們可以構建一個核心對象,然后在核心周圍創建一個裝飾器。由于裝飾對象與核心對象具有相同的接口,我們甚至可以使用其他裝飾對象包裝這個新對象。下面是它在 UML 中的樣子:

</b>
在這里,**核心**和所有裝飾器均實現一個特定的**接口**。裝飾器通過組合維護對接口另一個實例的引用。當被調用的時候,裝飾器會在調用其包裝的接口之前或之后進行一些額外的處理。包裝對象可以是另一個裝飾器,或者核心功能。
雖然多個裝飾者可以相互包裝,但處在所有裝飾器“中心”的對象提供了核心功能。
### 裝飾器例子
讓我們看一個網絡編程的例子。我們將使用一個TCP套接字。`socket.send()`方法獲取一串輸入字節,并將它們輸出到另一端的套接字。有很多庫可以接受套接字,并通過訪問此函數發送流數據。讓我們創建這樣一個對象;它是一個交互式shell,等待來自客戶端的連接,然后提示用戶輸入字符串:
```
import socket
def respond(client):
response = input("Enter a value: ")
client.send(bytes(response, 'utf8'))
client.close()
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind(('localhost',2401))
server.listen(1)
try:
while True:
client, addr = server.accept()
respond(client)
finally:
server.close()
```
`respond`函數接受套接字參數,并提示發送數據作為回復,然后發送。為了使用它,我們構建了一個服務器套接字,并告訴它監聽本地計算機的端口2401(我隨機選擇了這個端口)。當客戶連接時,它調用`respond`函數,該函數交互請求數據并恰當的回復。需要注意的重要一點是`respond`函數只關心套接字接口的兩種方法:發送`send`和關閉`clolse`。為了測試這個,我們可以編寫一個非常簡單的客戶端,它連接到同一個端口并在退出前輸出響應:
```
import socket
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client.connect(('localhost', 2401))
print("Received: {0}".format(client.recv(1024)))
client.close()
```
使用這些程序:
1. 在一個終端中啟動服務器。
2. 打開第二個終端窗口并運行客戶端。
3. 在服務器窗口的“輸入值:”提示中,鍵入一個值,然后按回車。
4. 客戶端將接收你鍵入的內容,將其打印到控制臺,然后退出。再次運行客戶端;服務器提示輸入第二個值。
現在,再看看我們的服務器代碼,我們看到兩個部分。`respond`函數將數據發送到套接字對象中。剩下的腳本負責創建套接字對象。我們將創建一對定制套接字行為的裝飾器,而不必擴展或修改套接字本身。
</b>
讓我們從一個“日志”裝飾器開始。該對象將數據發送到客戶端之前將數據輸出到服務器控制臺:
```
class LogSocket:
def __init__(self, socket):
self.socket = socket
def send(self, data):
print("Sending {0} to {1}".format(
data, self.socket.getpeername()[0]))
self.socket.send(data)
def close(self):
self.socket.close()
```
這個類修飾一個套接字對象,并向客戶端套接字展示`send`和`close`接口。一個好的裝飾器也會實現(并且可能是定制)剩下所有的套接字方法。它應該正確地實現所有待發送的參數(它實際上也接受可選的標記參數),但是讓我們的例子盡可能保持簡單!每當對此對象調用`send`方法時,它都會在使用原始套接字向客戶端發送數據之前將記錄輸出到屏幕上。我們只需要修改原始代碼中的一行就可以使用這個裝飾器。我們不用套接字調用`respond`,我們用修飾后的套接字調用它:
```
respond(LogSocket(client))
```
雖然這很簡單,但我們必須問問自己,為什么我們不擴展套接字類并重寫`send`方法。我們可以在寫完日志后,調用`super().send`去發送實際的內容。這種設計也沒有錯。當面臨在裝飾器和繼承之間進行選擇時,如果我們需要根據某些條件動態修改對象,我們應該只使用裝飾器。例如,如果服務器當前處于調試模式,我們想做的只是讓日志裝飾器生效。當我們有不止一種可選行為,裝飾器也比多重繼承好。舉個例子,我們可以寫另外一個裝飾器,每當我們調用`send`時,使用`gzip`來壓縮數據:
```
import gzip
from io import BytesIO
class GzipSocket:
def __init__(self, socket):
self.socket = socket
def send(self, data):
buf = BytesIO()
zipfile = gzip.GzipFile(fileobj=buf, mode="w")
zipfile.write(data)
zipfile.close()
self.socket.send(buf.getvalue())
def close(self):
self.socket.close()
```
此版本中的`send`方法在把傳入的數據壓縮后再發送給客戶。
</b>
現在我們有了這兩個裝飾器,我們可以動態地編寫代碼在它們之間切換不同的響應。這個例子并不完整,但它說明了我們可能遵循的混合和匹配裝飾者的邏輯:
```
client, addr = server.accept()
if log_send:
client = LoggingSocket(client)
if client.getpeername()[0] in compress_hosts:
client = GzipSocket(client)
respond(client)
```
這段代碼檢查一個名為`log_send`的假設配置變量。如果這個變量啟用了,它將套接字包裝在`LoggingSocket`裝飾器中。同樣,它檢查
已連接的客戶端是否在已知可接受壓縮內容的地址列表中。如果是這樣,它將客戶包裝在`GzipSocket`裝飾器中。注意根據配置和連接客戶端,可以不使用或使用任意一個或使用全部裝飾器。嘗試使用多重繼承來編寫這個,看看你有多困惑!
### python中的裝飾器
裝飾器模式在Python中很有用,但是還有其他選擇。例如,我們可以使用猴子修補,我們在第7章“Python面向對象的快捷方式”中討論過,可以獲得類似的效果。單一繼承,“可選”計算在一個大型方法中完成,也是一個選項,多重繼承不應該僅僅因為不適合前面看到的特定的例子而不被注銷!
</b>
在Python中,在函數上使用這種模式非常常見。正如我們在前面章節看到的,函數也是對象。事實上,函數裝飾器是如此普遍,以至于Python提供了一種特殊的語法,使得將這種裝飾器應用于函數變得容易。
</b>
例如,我們可以用一種更一般的眼光來看日志記錄示例。除了僅僅記錄套接字上調用`send`的日志,我們可能會發現記錄所有對函數或方法的調用和很有用處。下面的示例實現了這個功能的裝飾器:
```
import time
def log_calls(func):
def wrapper(*args, **kwargs):
now = time.time()
print("Calling {0} with {1} and {2}".format(
func.__name__, args, kwargs))
return_value = func(*args, **kwargs)
print("Executed {0} in {1}ms".format(
func.__name__, time.time() - now))
return return_value
return wrapper
def test1(a,b,c):
print("\ttest1 called")
def test2(a,b):
print("\ttest2 called")
def test3(a,b):
print("\ttest3 called")
time.sleep(1)
test1 = log_calls(test1)
test2 = log_calls(test2)
test3 = log_calls(test3)
test1(1,2,3)
test2(4,b=5)
test3(6,7)
```
這個裝飾函數與我們之前探索的例子非常相似;在那些例子里,裝飾器獲取一個類似套接字的對象,并創建一個新的類似套接字的對象。這里,我們的裝飾器獲取一個函數對象并返回一個新的函數對象。這段代碼由三個獨立的任務組成:
* 一個能接受另一個函數作為參數的函數`log_calls`
* 該函數(內部)定義了一個新函數,名為`wrapper`,在調用原始函數之前做一些額外的工作
* 返回這個新函數
三個示例函數演示了如何使用裝飾器。第三個函數包括調用一個睡眠方法以演示計時測試。我們將每個函數傳遞給裝飾器,它返回一個新函數。我們將這個新函數賦予原始變量名稱,高效地用修飾的函數替換原來的函數。
</b>
這個語法允許我們動態地構建修飾函數對象,就像我們在套接字示例所做的那樣;如果我們不替換這個名字,我們甚至可以保留不同情況下的裝飾和非裝飾版本。
</b>
通常這些裝飾器永久應用于不同的函數。在這種情況下,Python支持一種特殊的語法,在定義函數時應用裝飾器。當我們討論了屬性裝飾器時,我們已經看過這個語法;現在,讓我們了解它是如何工作的。
除了在方法定義之后應用decorator函數,我們可以使用`@decorator`語法一次完成所有操作:
```
@log_calls
def test1(a,b,c):
print("\ttest1 called")
```
這種語法的主要好處是,我們可以很容易地看到,函數已經在它被定義的時候被裝飾了。如果裝飾器后來被應用,讀代碼的人可能會忘記該函數已被更改。當回答諸如問題,“為什么我的程序日志函數調用出現在控制臺上?”,會變得很困難!然而,該語法只能應用于我們定義函數時,因為我們無法訪問其他模塊的源代碼。如果我們需要修飾其他人寫的第三方庫的一部分,我們必須使用早先的語法。
</b>
裝飾器語法遠比我們在這里看到的更多。我們不在這里涵蓋高級主題,你可以查閱Python參考手冊或其他教程了解更多信息。裝飾器可以被創建作為可調用的對象,而不是只是返回函數的函數。類也可以被裝飾;在這種情況下,裝飾器返回一個新類,而不是一個新函數。最后,裝飾器可以接受參數,在每個函數的基礎上定制它們。
## 觀察者模式
觀察者模式對于狀態監控和事件處理情況非常有用。這種模式允許給定對象被未知動態“觀察者”對象組監視。
</b>
每當核心對象上的值發生變化時,它都會通過調用`update()`方法讓所有觀察者對象知道,有一個變更發生了。每當核心對象發生變化時,每個觀察者可以負責不同的任務;核心對象并不知道或不關心那些任務是什么,觀察者通常也不知道或者不關心其他觀察者在做什么。
</b>
下面是觀察者模式的UML:

### 觀察者例子
觀察者模式在冗余備份系統中可能很有用。我們可以寫一個維護一些特定值的核心對象,然后一個或多個觀察者創建核心對象的序列化副本。這些副本可能存儲在遠程主機的數據庫中或本地文件中。讓我們使用屬性實現核心對象:
```
class Inventory:
def __init__(self):
self.observers = []
self._product = None
self._quantity = 0
def attach(self, observer):
self.observers.append(observer)
@property
def product(self):
return self._product
@product.setter
def product(self, value):
self._product = value
self._update_observers()
@property
def quantity(self):
return self._quantity
@quantity.setter
def quantity(self, value):
self._quantity = value
self._update_observers()
def _update_observers(self):
for observer in self.observers:
observer()
```
該對象有兩個屬性,當我們給它們設置后,將在屬性上調用`_update_observers`方法。這個方法所做的就是歷遍所有有效的觀察者,讓每個觀察者知道有些事情已經改變了。在這種情況下,我們直接調用觀察者對象;對象必須實現`__call__`方法來處理更新。這在許多面向對象編程語言中是不可能的,但是這是一個有用的快捷方式,有助于我們的代碼更加易讀。
</b>
現在讓我們實現一個簡單的觀察者對象;它只是打印出一些狀態到控制臺:
```
class ConsoleObserver:
def __init__(self, inventory):
self.inventory = inventory
def __call__(self):
print(self.inventory.product)
print(self.inventory.quantity)
```
這里沒有什么特別令人興奮的;觀察到的對象在初始化方法中被設置,
當觀察者被調用時,我們會做“一些事情”,我們可以在一個交互式控制臺測試這個觀察者對象:
```
>>> i = Inventory()
>>> c = ConsoleObserver(i)
>>> i.attach(c)
>>> i.product = "Widget"
Widget
0
>>> i.quantity = 5
Widget
5
```
將觀察者附加到`inventory`對象后,每當我們更改兩個觀察屬性的一個屬性,觀察者都被調用,其動作被調用。我們甚至可以添加兩個不同的觀察者實例:
```
>>> i = Inventory()
>>> c1 = ConsoleObserver(i)
>>> c2 = ConsoleObserver(i)
>>> i.attach(c1)
>>> i.attach(c2)
>>> i.product = "Gadget"
Gadget
0
Gadget
0
```
這次我們更換產品名稱時,有兩組輸出,每組對應一個觀察者。這里的關鍵思想是,我們可以很容易地添加完全不同類型的觀察者,同時備份文件、數據庫或互聯網應用程序中的數據。
</b>
觀察者模式將被觀察的代碼與執行觀察的代碼分離。如果我們不使用這種模式,我們將不得不在每個屬性安排代碼處理可能出現的不同情況;登錄控制臺,更新數據庫或文件,等等。每個任務的代碼都與被觀察對象混合在一起。維護它將是一場噩夢,稍后補充新的監控功能也會很痛苦。
## 策略模式
策略模式是面向對象編程抽象的一個常見演示。該模式實現了對單個問題提供不同解決方案,每個都在不同的對象中。然后,客戶端代碼可以在運行時動態地選擇最合適的方案。
</b>
通常,不同的算法有不同的權衡;一個可能比另一種快,但使用更多的內存,而當存在多個處理器或提供分布式系統時,第三種算法可能最合適的。策略模式的UML如下:

連接到策略模式的用戶代碼只需要知道它正在處理抽象接口。選擇的實際實現執行相同的任務,但方式不同;不管怎樣,接口是相同的。
### 策略例子
策略模式的典型例子是排序例程;這些年來,已經發明了許多算法來對對象集合進行排序;快排序、合并排序和堆排序都是具有不同特點的快速排序算法,根據輸入的大小和類型、它們的輸出順序以及系統的要求,每一個算法都有自己的用處。
</b>
如果我們有需要對集合進行排序的客戶端代碼,我們可以將它傳遞給一個含有`sort()`方法的對象。該對象可以是`QuickSorter`或`MergeSorter`對象,但是這兩種情況下的結果都是一樣的:排序列表。排序策略是從調用代碼中抽象出來的,使之模塊化和可替換。
</b>
當然,在Python中,我們通常只調用`sorted`函數或`list.sort`方法,并相信它會以近乎最優的方式進行排序。所以,我們真的需要看一個更好的例子。
</b>
讓我們考慮一個桌面壁紙管理器。當圖像顯示在桌面背景上,它可以以不同的方式適應屏幕大小。例如,假設圖像比屏幕小,它可以平鋪在屏幕上,以它為中心,或者按比例縮放。還有其他更復雜的策略可以使用,例如放大到最大高度或寬度,將其與純色、半透明色或漸變背景色,或其他操作綁定。我們可能想稍后添加這些策略,讓我們從基本策略開始。
</b>
我們的策略對象需要兩個輸入;要顯示的圖像,以及屏幕寬度和高度構成的元組。它們每一個根據給定的策略對圖像進行處理,返回一個屏幕大小的新圖像。你需要為本例使用`pip3`安裝`pillow`模塊:
```
from PIL import Image
class TiledStrategy:
def make_background(self, img_file, desktop_size):
in_img = Image.open(img_file)
out_img = Image.new('RGB', desktop_size)
num_tiles = [
o // i + 1 for o, i in
zip(out_img.size, in_img.size)
]
for x in range(num_tiles[0]):
for y in range(num_tiles[1]):
out_img.paste(
in_img,
(
in_img.size[0] * x,
in_img.size[1] * y,
in_img.size[0] * (x+1),
in_img.size[1] * (y+1)
)
)
return out_img
class CenteredStrategy:
def make_background(self, img_file, desktop_size):
in_img = Image.open(img_file)
out_img = Image.new('RGB', desktop_size)
left = (out_img.size[0] - in_img.size[0]) // 2
top = (out_img.size[1] - in_img.size[1]) // 2
out_img.paste(
in_img,
(
left,
top,
left+in_img.size[0],
top + in_img.size[1]
)
)
return out_img
class ScaledStrategy:
def make_background(self, img_file, desktop_size):
in_img = Image.open(img_file)
out_img = in_img.resize(desktop_size)
return out_img
```
這里我們有三種策略,每一種都使用`PIL`來完成它們的任務。單個
策略有一個`make_background`方法,接受相同的參數集。一旦選擇后,可以調用適當的策略來創建正確大小的桌面圖像。`TiledStrategy`遍輸入圖像的數量,根據圖像的寬度和高度,將它們重復復制到新的位置。`CenteredStrategy`規劃出需要在圖像的四個邊留出多少空間,使其居中。`ScaledStrategy`強制圖像輸出大小(忽略縱橫比)。
</b>
考慮一下如何在沒有策略模式下在這些選項中進行切換。我們需要將所有代碼放入一個宏大的方法中,使用一個笨拙的`if`語句來選擇預期的語句。每次我們想添加一個新的策略,我們必須使這個方法更加笨拙。
### python中的策略
前面提到的策略模式的典型實現,雖然在大多數面向對象的庫中非常常見,但在Python編程中已經很少見到。
</b>
這些類各自代表只提供一個單一函數的對象。我們可以很容易地調用函數`__call__`并直接調用對象。由于沒有與對象相關聯的其他數據,我們只需要創建一組頂級函數,并將其替代我們的策略進行傳遞。
</b>
因此,設計模式哲學的反對者會說,“因為Python已經有一流的函數,策略模式是不必要的”。事實上,Python一流的函數允許我們以更直接的方式實現策略模式。知道模式的存在仍然可以幫助我們為程序選擇正確的設計,但是使用更可讀的語法來實現它。策略模式,或它的頂層函數實現,當我們需要允許客戶端代碼或終端用戶從同一接口的多個實現中進行選擇時,才應該被使用。
## 狀態模式
狀態模式在結構上類似于策略模式,但是它的意圖和目的非常不同。狀態模式的目標是表示狀態轉換系統:一個對象明顯處于特定狀態的系統,并且某些活動可能會將它推向不同的狀態。
</b>
為了實現這一點,我們需要一個提供接口的管理器或上下文類,用于切換狀態。在內部,這個類包含一個指向當前狀態的指針;每個狀態知道它被允許進入哪些其他狀態,這些狀態遷移取決于對其調用的操作。
</b>
所以我們有兩種類型的類,上下文類和多個狀態類。上下文類維護當前狀態,以及去其他狀態類的動作。狀態類通常對調用上下文的其他對象是隱藏的;它就像一個黑匣子,碰巧在內部執行狀態管理。下面是它在UML中的樣子:

### 狀態例子
為了說明狀態模式,讓我們構建一個XML解析工具。上下文類將作為解析器本身。它將使用一個字符串作為輸入,并將工具設置為初始解析狀態。不同解析狀態將吃掉字符,尋找特定的值,并且當找到該值時,更改為另外一個不同狀態。目標是為每個標簽及其內容創建一棵節點對象樹。為了易于管理,我們將解析的XML的子集只有標簽和標簽名稱。我們將無法處理上標簽的屬性。它將解析標簽的文本內容,但不會嘗試解析文本內部有標簽的“混合”內容。下面是一個“簡化的XML”文件的例子,我們將能夠解析:
```
<book>
<author>Dusty Phillips</author>
<publisher>Packt Publishing</publisher>
<title>Python 3 Object Oriented Programming</title>
<content>
<chapter>
<number>1</number>
<title>Object Oriented Design</title>
</chapter>
<chapter>
<number>2</number>
<title>Objects In Python</title>
</chapter>
</content>
</book>
```
在我們查看狀態和解析器之前,讓我們考慮一下這個程序的輸出。我們知道我們想要一棵節點對象樹,但是節點看起來像什么?顯然它需要知道它正在解析的標簽的名稱,因為它是一棵樹,它應該維護一個指向父節點的和該節點的順序子節點列表的指針。有些節點有文本值,但不是全部。讓我們先看看這個節點類:
```
class Node:
def __init__(self, tag_name, parent=None):
self.parent = parent
self.tag_name = tag_name
self.children = []
self.text=""
def __str__(self):
if self.text:
return self.tag_name + ": " + self.text
else:
return self.tag_name
```
該類在初始化時設置了默認屬性值。`__str__`方法用來幫助我們完成時可視化樹的結構。
</b>
現在,看看示例文檔,我們需要考慮我們解析器可能在的狀態。很明顯,它將從一個沒有節點被處理的狀態開始。我們需要一個處理開始標簽和結束標簽的狀態。當我們在一個帶有文本內容的標簽里,我們也必須把它作為一個單獨的狀態來處理。
</b>
切換狀態可能很棘手;我們如何知道下一個節點是否是一個開始標記、結束標簽,還是文本節點?我們可以在每個狀態放一點邏輯來解決這個問題,但是創建一個新的狀態更有意義,它唯一的目的是指示我們將切換到哪一個狀態。如果我們稱這種過渡狀態為子節點,我們最終得到以下狀態:
* **FirstTag**
* **ChildNode**
* **OpenTag**
* **CloseTag**
* **Text**
`FirstTag`狀態將切換到`ChildNode`,然后`ChildNode`負責決定切換到其他三個狀態中的哪一個;當這些狀態結束時,它們會切換回到`ChildNode`。下面的狀態轉換圖顯示了可能的狀態變化:

狀態負責收集“字符串的剩余部分”,盡它們所知去處理,然后告訴解析器處理其余的。讓我們首先構造`Parser`類:
```
class Parser:
def __init__(self, parse_string):
self.parse_string = parse_string
self.root = None
self.current_node = None
self.state = FirstTag()
def process(self, remaining_string):
remaining = self.state.process(remaining_string, self)
if remaining:
self.process(remaining)
def start(self):
self.process(self.parse_string)
```
初始化方法在類中設置了一些各個狀態將訪問的變量。`parse_string`實例變量是我們試圖解析的文本。根`root`節點是XML結構中的“頂部”節點。當前節點`current_node`實例變量是我們當前正在添加子變量的變量。
</b>
這個解析器的重要特性是`process`方法,它接受剩余的字符串,并將其傳遞到當前狀態。解析器(`self`參數)也被傳遞到狀態的`process`方法中,以便狀態可以操作它。當狀態完成處理,它將返回未解析的剩余字符串。然后解析器在剩余字符串上遞歸調用`process`方法,來構造樹的其余部分。現在,讓我們看看第一個`FirstTag`狀態:
```
class FirstTag:
def process(self, remaining_string, parser):
i_start_tag = remaining_string.find('<')
i_end_tag = remaining_string.find('>')
tag_name = remaining_string[i_start_tag+1:i_end_tag]
root = Node(tag_name)
parser.root = parser.current_node = root
parser.state = ChildNode()
return remaining_string[i_end_tag+1:]
```
這個狀態發現第一個標簽上打開和關閉角括號的索引(i_代表索引)。你可能認為這種狀態是不必要的,因為XML需要開始標記前沒有文本。然而,可能仍然會有空格;這就是為什么我們要搜索開角括號,而不是假設它是文檔中的第一個字符。請注意,這段代碼假設輸入文件是有效的。一個正確的實現是嚴格測試無效輸入,會試圖恢復或顯示一個描述清晰的錯誤消息。
</b>
方法提取標記的名稱,并將其分配給解析器。它還將其分配給當前節點`current_node`,因為我們將要當前節點添加下一個子節點。
</b>
接下來是重要的部分:方法將解析器對象上的當前狀態轉換為子節點`ChildNode`狀態。然后它返回字符串的剩余部分(在打開標簽之后的)以允許它處理。
</b>
子節點`ChildNode`狀態看起來相當復雜,結果卻只是查詢一個簡單的條件:
```
class ChildNode:
def process(self, remaining_string, parser):
stripped = remaining_string.strip()
if stripped.startswith("</"):
parser.state = CloseTag()
elif stripped.startswith("<"):
parser.state = OpenTag()
else:
parser.state = TextNode()
return stripped
```
`strip()`調用移除字符串中的空白。然后解析器確定下一個項目是開始或結束標簽,或一串文本。取決于可能發生的情況,它將解析器設置為特定狀態,然后告訴它解析字符串的剩余部分。
</b>
`OpenTag`狀態類似于`FirstTag`狀態,只是它為上一個當前節點`current_node`對象的子節點`Child`上加了一個新創建的節點,并將其設置為新的當前節點`current_node`。在繼續之前,它會將處理器重新置于子節點狀態:
```
class OpenTag:
def process(self, remaining_string, parser):
i_start_tag = remaining_string.find('<')
i_end_tag = remaining_string.find('>')
tag_name = remaining_string[i_start_tag+1:i_end_tag]
node = Node(tag_name, parser.current_node)
parser.current_node.children.append(node)
parser.current_node = node
parser.state = ChildNode()
return remaining_string[i_end_tag+1:]
```
`CloseTag`狀態基本上是相反的;它設置解析器的當前節點`current_node`返回到父節點,這樣外部標簽中的任何其他子節點都可以添加到其中:
```
class CloseTag:
def process(self, remaining_string, parser):
i_start_tag = remaining_string.find('<')
i_end_tag = remaining_string.find('>')
assert remaining_string[i_start_tag+1] == "/"
tag_name = remaining_string[i_start_tag+2:i_end_tag]
assert tag_name == parser.current_node.tag_name
parser.current_node = parser.current_node.parent
parser.state = ChildNode()
return remaining_string[i_end_tag+1:].strip()
```
這兩個斷言`assert`語句有助于確保解析字符串一致。方法最后的`if`語句確保處理結束后處理器中止。如果節點的父節點是`None`,意味著我們正在根節點上工作。
</b>
最后,`TextNode`狀態非常簡單地提取下一個結束標記之前的文本
將其設置為當前節點上的值:
```
class TextNode:
def process(self, remaining_string, parser):
i_start_tag = remaining_string.find('<')
text = remaining_string[:i_start_tag]
parser.current_node.text = text
parser.state = ChildNode()
return remaining_string[i_start_tag:]
```
現在我們只需要在我們創建的解析器對象上設置初始狀態。初始狀態是一個`FirstTag`對象,所以只需將以下內容添加到`__init__`方法中:
```
self.state = FirstTag()
```
為了測試這個類,讓我們添加一個主腳本,從命令行打開一個文件,
解析它,并打印節點:
```
if __name__ == "__main__":
import sys
with open(sys.argv[1]) as file:
contents = file.read()
p = Parser(contents)
p.start()
nodes = [p.root]
while nodes:
node = nodes.pop(0)
print(node)
nodes = node.children + nodes
```
這段代碼打開文件,加載內容,并解析結果。然后按順序打印每個節點及其子節點。我們最初在節點類上添加的`__str__`方法負責格式化打印節點。如果我們在早期的例子上運行腳本,它輸出如下樹:
```
book
author: Dusty Phillips
publisher: Packt Publishing
title: Python 3 Object Oriented Programming
content
chapter
number: 1
title: Object Oriented Design
chapter
number: 2
title: Objects In Python
```
將它與原始的簡化的XML文檔進行比較,解析器工作正常。
### 狀態VS策略
狀態模式看起來非常類似于策略模式;事實上,兩者的UML圖是相同的。實現也是相同的;我們甚至可以把我們的狀態寫成一級函數,而不是把它們包裝成對象,正如對策略模式的建議。
</b>
雖然這兩種模式具有相同的結構,但它們解決完全不同的問題。策略模式用于在運行時選擇算法;一般來說,對于特定的用例,將只選擇其中一種算法。另一方面,狀態模式被設計成允許在不同狀態之間動態地切換,隨著一些過程的發展。在代碼中,主要區別在于策略模式通常不知道其他策略對象。在狀態模式中,狀態或上下文需要知道它可以切換到其他哪個狀態。
### 策略轉協程
狀態模式是狀態轉換問題的典型面向對象解決方案。然而,這種模式的語法相當冗長。你還記得我們在第9章“迭代器模式”中構建的正則表達式日志文件解析器?你可以通過將對象構造為協程得到類似的效果。這是一個偽裝的狀態轉換問題。這種實現和定義在狀態模型定義對象(或函數)實現之間的主要區別是協程解決方案允許我們在語言結構中編碼更多的樣板文件。這兩個實現,兩者都沒有本質上的優勢,但是你可以
發現對于給定的“可讀”定義,協程更加可讀(首先,你必須理解協程的語法!)。
## 單例模式
單例模式是最有爭議的模式之一;許多人指責它是一種“反模式”,一種應該避免而不是被提倡的模式。在Python中,如果有人使用單例模式,他們幾乎肯定在做一些錯的事情,可能是因為他們之前用的是更嚴格的編程語言。
</b>
那為什么還要討論它呢?單例模式是所有設計模式中最著名的一種。它不僅在過度面向對象的語言中非常有用,而且也是傳統面向對象編程的重要組成部分。更重要的是,單例模式背后的想法是有用的,即使我們在Python中完全可以用不同的方式實現這個想法。
</b>
單例模式背后的基本想法是只允許某些對象存在一個實例。通常,這個對象是一種管理者類,類似于我們在第5章“何時使用面向對象編程”中討論的類。這樣的對象經常需要被各種各樣的其他對象引用,并將引用傳遞給那些需要它們的管理者對象的方法和構造函數中,這使得代碼難以閱讀。
</b>
相反,當使用單例時,這些對象請求的是來自這個類的管理者對象的唯一一個實例,所以不需要傳遞對它的引用。UML圖沒有完全描述它,但是這里UML仍然是完整的:

在大多數編程環境中,通常讓構造函數私有構建單例類(這樣就沒有人可以創建它的其他實例了),然后提供獲取該單例的靜態方法。此方法在第一次調用它時創建一個新實例,然后每次再次調用它時返回相同的實例。
### 單例實現
Python沒有私有構造函數,但是出于這個目的,它有一些更好的東西。我們可以使用`__new__`類方法來確保只有一個實例可以被創建:
```
class OneOnly:
_singleton = None
def __new__(cls, *args, **kwargs):
if not cls._singleton:
cls._singleton = super(OneOnly, cls
).__new__(cls, *args, **kwargs)
return cls._singleton
```
當調用`__new__`時,它通常會創建該類的一個新實例。當我們覆蓋它,我們首先檢查是否已經創建了單例;如果沒有,我們調用`super`來創建這個單例。因此,每當我們調用`OneOnly`的構造函數時,我們總是得到完全相同的實例:
```
>>> o1 = OneOnly()
>>> o2 = OneOnly()
>>> o1 == o2
True
>>> o1
<__main__.OneOnly object at 0xb71c008c>
>>> o2
<__main__.OneOnly object at 0xb71c008c>
```
這兩個對象位于同一內存地址;因此,它們是相同的對象。這個特定的實現不是非常透明,因為我們并不清楚我們創建的是單例對象。如果我們調用構造函數,期待的是創建一個新的對象實例;在這種情況下,單例并不滿足我們的要求。如果我們真的認為我們需要一個單例,也許在類上加上文檔說明,可以緩解這個問題。
</b>
但我們不需要它。Python程序員不贊成強迫他們的代碼用戶進入一個特定的心態。我們可能認為一個類只需要一個實例,但是其他程序員可能有不同的想法。單例可能會干擾分布式計算、并行編程和自動化測試。在所有這些情況下,擁有一個特定對象的多個或可選實例會非常有用,即使“正常”操作可能永遠不需要這么一個對象。
</b>
模塊變量可以模擬單例。
</b>
通常,在Python中,使用模塊變量模擬單例是足夠的。它不像一個單例那樣“安全”,因為人們在任何時候都可以重新分配這些變量,但是就像我們在第2章“Python中的對象”中討論的私有變量一樣,這在Python中是可以被接受的。如果某人有改變這些變量的原因,我們為什么要阻止他們?人們也不會停止實例化對象的多個實例,如果他們有這么做的正當理由,為什么要干涉呢?
</b>
理想情況下,我們應該給他們一個訪問“默認單例”值的機制,同時也允許他們在需要時創建其他實例。從技術上講,它根本不是一個單例,它提供了類似單例行為的Python機制。
</b>
為了使用模塊變量代替單例,我們實例化一個已經定義好的類的實例。我們可以使用單例來改進我們的狀態模式來。我們可以創建一個始終可訪問的模塊變量,就不用每次當我們更改狀態時需要創建一個新的對象:
```
class FirstTag:
def process(self, remaining_string, parser):
i_start_tag = remaining_string.find('<')
i_end_tag = remaining_string.find('>')
tag_name = remaining_string[i_start_tag+1:i_end_tag]
root = Node(tag_name)
parser.root = parser.current_node = root
parser.state = child_node
return remaining_string[i_end_tag+1:]
class ChildNode:
def process(self, remaining_string, parser):
stripped = remaining_string.strip()
if stripped.startswith("</"):
parser.state = close_tag
elif stripped.startswith("<"):
parser.state = open_tag
else:
parser.state = text_node
return stripped
class OpenTag:
def process(self, remaining_string, parser):
i_start_tag = remaining_string.find('<')
i_end_tag = remaining_string.find('>')
tag_name = remaining_string[i_start_tag+1:i_end_tag]
node = Node(tag_name, parser.current_node)
parser.current_node.children.append(node)
parser.current_node = node
parser.state = child_node
return remaining_string[i_end_tag+1:]
class TextNode:
def process(self, remaining_string, parser):
i_start_tag = remaining_string.find('<')
text = remaining_string[:i_start_tag]
parser.current_node.text = text
parser.state = child_node
return remaining_string[i_start_tag:]
class CloseTag:
def process(self, remaining_string, parser):
i_start_tag = remaining_string.find('<')
i_end_tag = remaining_string.find('>')
assert remaining_string[i_start_tag+1] == "/"
tag_name = remaining_string[i_start_tag+2:i_end_tag]
assert tag_name == parser.current_node.tag_name
parser.current_node = parser.current_node.parent
parser.state = child_node
return remaining_string[i_end_tag+1:].strip()
first_tag = FirstTag()
child_node = ChildNode()
text_node = TextNode()
open_tag = OpenTag()
close_tag = CloseTag()
```
我們所做的只是創建可以重用的各種狀態類的實例。注意我們如何在類中訪問這些模塊變量,甚至是在這些變量被定義之前?這是因為類內部的代碼直到調用該方法才會被執行,此時,整個模塊都已經被定義了。
</b>
本例的不同之處在于,我們沒有去創建一組浪費內存、必須被垃圾收集的新實例,我們為每一個狀態重用一個單一的狀態對象。即使多個解析器同時運行,也只有這些狀態類需要被使用。
</b>
當我們創建最初的狀態解析器時,你可能想知道為什么我們沒有在每個單獨的狀態下將解析器對象傳遞給`__init__`,而是像我們已經做的那樣把它傳遞到過程方法中。當時的狀態可能隨后被作為`self.parser的引用。這是一個完全有效的狀態模式的實現,但是它不允許利用單例模式。如果這些狀態對象維護對解析器的一個引用,那么它們不能同時用于引用其他解析器。
</b>
> 記住,這是兩種不同的模式,目的不同;單例對有利于狀態模式的實現并不意味著這兩種模式是相關的。
## 模板模式
模板模式對于刪除重復代碼非常有用;這是在第5章*何時使用面向對象編程*中討論的**不要重復你自己**原則的一個實現。這種設計模式適用于:我們有幾個不同的任務,它們有一些共同的、但不是全部的步驟。這些共同步驟在基類中實現,不同的步驟則在子類中被重寫,以提供自定義行為。從某些方面來說,這像一個廣義的策略模式,只是算法的相似部分使用一個基礎類共享。下圖是這種模式的UML圖:

### 模板例子
讓我們創建一個汽車銷售報告員作為例子。我們將銷售記錄存儲在一個 SQLite 數據庫表。SQLite 是一個簡單的基于文件的數據庫引擎,它允許我們使用 SQL 語法存儲記錄。Python 3在其標準庫中包含 SQLite ,所以不需要額外的模塊。
</b>
我們需要完成兩個普通的任務:
* 選擇所有新車銷售記錄,并將它們以逗號分隔格式輸出到屏幕上
* 輸出所有銷售人員及其總銷售額的逗號分隔列表,并將它保存到可以導入電子表格的文件中
這些任務看起來完全不同,但是它們有一些共同的特點。兩者都需要執行以下步驟:
1. 連接到數據庫。
2. 構建一個對新車或總銷售額的查詢。
3. 發出查詢。
4. 將結果格式化為逗號分隔的字符串。
5. 將數據輸出到文件或電子郵件中。
這兩個任務的查詢構造和輸出步驟不同,但是其余步驟是相同的。我們可以使用模板模式將公共步驟放置到基類中,在兩個子類中定義不同步驟。在我們開始之前,讓我們創建一個數據庫,并使用一些 SQL 語句往數據庫中放入一些數據:
```
import sqlite3
conn = sqlite3.connect("sales.db")
conn.execute("CREATE TABLE Sales (salesperson text, "
"amt currency, year integer, model text, new boolean)")
conn.execute("INSERT INTO Sales values"
" ('Tim', 16000, 2010, 'Honda Fit', 'true')")
conn.execute("INSERT INTO Sales values"
" ('Tim', 9000, 2006, 'Ford Focus', 'false')")
conn.execute("INSERT INTO Sales values"
" ('Gayle', 8000, 2004, 'Dodge Neon', 'false')")
conn.execute("INSERT INTO Sales values"
" ('Gayle', 28000, 2009, 'Ford Mustang', 'true')")
conn.execute("INSERT INTO Sales values"
" ('Gayle', 50000, 2010, 'Lincoln Navigator', 'true')")
conn.execute("INSERT INTO Sales values"
" ('Don', 20000, 2008, 'Toyota Prius', 'false')")
conn.commit()
conn.close()
```
希望你即使不懂 SQL 也能看到這里發生了什么;我們已經創建了一個保存數據的表,并使用六條插入語句添加銷售記錄。數據存儲在名為`sales.db`的文件中。現在我們有了一個可以用于開發模板模式的樣本數據。
</b>
既然我們已經概述了模板必須執行的步驟,我們可以開始定義包含這些步驟的基類。每一步都有自己的方法(使得任何一個步驟都很容易地有選擇地被重寫),我們還有一個可以依次調用這些步驟的管理方法。這些方法沒有任何內容,它們看起來是這樣的:
```
class QueryTemplate:
def connect(self):
pass
def construct_query(self):
pass
def do_query(self):
pass
def format_results(self):
pass
def output_results(self):
pass
def process_format(self):
self.connect()
self.construct_query()
self.do_query()
self.format_results()
self.output_results()
```
`process_format`方法是外部客戶調用的主要方法。它確保每個步驟都按順序執行,但不關心該步驟是否在這個類或子類中實現。舉個例子,我們知道兩個類的三個方法將是相同的:
```
import sqlite3
class QueryTemplate:
def connect(self):
self.conn = sqlite3.connect("sales.db")
def construct_query(self):
raise NotImplementedError()
def do_query(self):
results = self.conn.execute(self.query)
self.results = results.fetchall()
def format_results(self):
output = []
for row in self.results:
row =[str(i) for i in row]
output.append(", ".join(row))
self.formatted_results = "\n".join(output)
def output_results(self):
raise NotImplementedError()
```
為了幫助實現子類,沒有定義的兩種方法引發`NotImplementedError`。這是在Python中指定抽象接口的常見方式,當抽象基類看起來太重時。這些方法可以具有空的實現(帶pass),或者可以完全不使用。然而,拋出`NotImplementedError`有利于程序員理解,這個類將被子類化并且這些方法被重寫;如果使用空方法或不定義方法,我們很難識別需要實現的需求,一旦我們忘記實現它們,則很難調試。
</b>
現在我們有了一個模板類來處理無聊的細節,但是它足夠靈活,允許執行和格式化各種各樣的查詢。最棒的是,如果我們想要將我們的數據庫引擎從SQLite更改為另一種數據庫引擎(例如,py-postgresql),我們只需要在這個模板類中完成它,而不必修改我們可能已經編寫的兩(或兩百)個子類。現在讓我們來看看實際的類:
```
import datetime
class NewVehiclesQuery(QueryTemplate):
def construct_query(self):
self.query = "select * from Sales where new='true'"
def output_results(self):
print(self.formatted_results)
class UserGrossQuery(QueryTemplate):
def construct_query(self):
self.query = ("select salesperson, sum(amt) " +
" from Sales group by salesperson")
def output_results(self):
filename = "gross_sales_{0}".format(
datetime.date.today().strftime("%Y%m%d")
)
with open(filename, 'w') as outfile:
outfile.write(self.formatted_results)
```
考慮到它們正在做的事情,這兩個類實際上很短:連接到數據庫,執行查詢,格式化結果,并輸出結果。超類負責重復的工作,但是讓我們很容易定義那些因任務而異的步驟。此外,我們還可以輕松更改在基類中提供的步驟。例如,如果我們想輸出其他東西,而不是逗號分隔的字符串(例如:要上傳到網站的HTML報告),我們仍然可以重寫`format_results`。
## 摘要
本章舉例詳細討論了幾種常見的設計模式、UML圖,以及討論了Python和靜態類型面向對象語言之間的差異。裝飾器模式通常使用
Python更通用的修飾語法實現。觀察者模式是一種有用的方法,可以將事件與對這些事件采取的行動分離開來。策略模式允許選擇不同的算法來完成相同的任務。狀態模式看起來相似,但被用來表示系統,該系統使用明確定義的行動在不同狀態間切換。單例模式,流行于一些靜態類型語言,在Python中,幾乎總是反模式的。
</b>
下一章,我們將結束對設計模式的討論。