本節書摘來自華章出版社《python程式設計實戰:運用設計模式、并發和程式庫建立高品質程式》一 書中的第2章,第2.7節,作者:(美) mark summerfield,更多章節内容可以通路雲栖社群“華章計算機”公衆号檢視。
若想用一個對象來代表另一個對象,則可使用“代理模式”(proxy pattern)。《design patterns》一書舉了四個用例。第一個用例是“遠端代理”(remote proxy):用本地對象來代表遠端對象。rpyc程式庫就是個很好的例子,它可以在伺服器端建立對象,并在一台或多台用戶端中建立針對這些對象的代理(6.2節将會介紹這個程式庫)。第二個用例是“虛代理”(virtual proxy),用來建立能夠代表複雜對象的輕量級對象,隻在确有必要時才會真正去建立那個複雜對象。本節所舉的例子就是這種代理。第三個用例是“保護代理”(protection proxy),可根據用戶端的通路權限來确定不同的通路級别。最後一種用例是“智能引用”(smart reference),可用來在“通路對象時執行額外操作”(performs additional actions when an object is accessed)。這些代理模式都可以采用同一套編碼方式來實作,其中第四種代理還可以通過描述符來實作(比方說,利用@property修飾器,以屬性來取代普通對象)。
代理模式也可用于單元測試。例如,受測代碼所需通路的資源并非随時可用,或是所需使用的類尚未開發完畢而依然不完整,那就可以考慮為資源或類建立代理,令代理對象提供所有接口,并且用“樁”(stub)來表示那些缺失的功能。這種做法非常有用,python 3.3包含了unittest.mock庫,可用來建立“模拟對象”(mock object),并設定“樁”來表示缺失的方法。
本節範例所假定的使用場景是:我們可能會建立很多圖像,但最後隻會用到其中一張。image子產品與功能相仿但速度更快的cyimage子產品都可以建立圖像(第3.12節及5.3節分别講解二者),但它們一開始就會把圖像建立在記憶體裡。而我們隻會用到這些圖像中的一張,是以更好一些的辦法是:建立許多輕量級圖像代理,然後隻在真正有需要時才去建立實際圖像。
除了構造器之外,image.image類的接口還有十個方法:load()、save()、pixel()、set_pixel()、line()、rectangle()、ellipse()、size()、subsample()、scale()。(此外,還有一些靜态的便捷方法以及等效的子產品函數,例如image.image.color_for_name()及image.color_for_name()。)
代理類隻需要實作image.image中我們必須用到的那些方法即可。首先來看代理類的用法。本節範例代碼選自imageproxy1.py,繪制出的圖像如圖2.8所示。

首先,需要用image子產品的color_for_name()函數建立一些顔色常量。
上面這段代碼先建立了imageproxy對象,我們在建立時把需要使用的image類傳給了構造器。然後又在對象上面繪制了一些内容,最後将繪制好的圖像存儲起來。假如建立圖像時調用的不是imageproxy()而是image.image(),那麼剩下的繪制操作依然能照常執行。但是,采用了圖像代理之後,隻有在調用save()方法時才會去建立真正的圖像,這樣的話,在執行儲存操作之前,建立圖像的開銷(無論是記憶體開銷還是處理開銷)就變得非常小,若是最後不儲存圖像而直接将其丢棄,那麼損失也會很低。若用image.image來建立,則一開始就需要很大開銷(也就是說,一開始就要建立大小為width×height的數組用以儲存顔色值),而且在繪制時還需要執行很多處理工作(例如在填充矩形時,要計算出需要填充的像素,并把它們的顔色都設定好),即便最後決定要丢棄這張圖像,也還是得執行丢棄之前那些操作。
隻要imageproxy所提供的接口足夠用,它就能代表image.image(如果構造時傳入了支援image接口的其他類,那麼也能代表那個類)。imageproxy并不儲存圖像,它儲存的是一份元組清單,每個元組表示一條指令,其首個元素是函數或非綁定方法,其餘元素是傳給調用函數或方法的參數。
建立imageproxy對象時,必須指定長和寬(以便按此大小來建立圖像)或檔案名。如果用檔案名建立imageproxy,那麼就會儲存一條指令,這條指令旨在調用image.image()構造器,構造器所用的width及height參數都是none,而filename參數則是建立imageproxy時所傳入的檔案名,imageproxy.load()方法所對應的指令與此相同。建立好imageproxy對象之後,如果又調用了imageproxy.load()方法,那麼先前的全部指令都将丢棄,self.commands指令清單中隻會留下一條建立圖像的指令。若用給定的長度與寬度來建立imageproxy對象,則對應的指令中儲存的是image.image()構造器,構造器所用的width及height參數是建立時所傳入的長度與寬度。
如果調用了代理對象所不支援的方法(比如pixel()),那麼python就會發現這個方法找不到,進而自動抛出attributeerror,而這正是我們想要的效果。還有一種處理辦法:如果代理對象不支援将要調用的方法,那就把實際的image對象建立出來,并在此對象上執行後續操作。(imageproxy2.py程式采用這種辦法,該程式的代碼沒有列在本節中。)
image.image類的接口中有四個繪制方法:line()、rectangle()、ellipse()、set_pixel()。我們的imageproxy類完全支援這些方法,但并不當場執行操作,而是把操作及其參數做成一條指令,放在self.commands清單裡。
隻有在儲存時才需要建立真正的圖像,也隻有此時才會有真正的處理開銷及記憶體開銷。imageproxy的設計方式決定了其首個指令一定是建立圖像(可能是根據長寬來建立,也可能是從既有檔案中加載)。是以我們采用特殊方式來處理第一條指令:将執行該指令所得的傳回值儲存起來,這個傳回值肯定是個image.image或cyimage.image。然後,周遊剩下的指令,并依次執行之,由于執行的都是非綁定方法,是以需要把image變量作為首個參數(也就是self)傳進去。最後,調用image.image.save()方法,儲存圖像。
雖說image.image.save()方法在發生錯誤時會抛出異常,但這個方法本身是沒有傳回值的。然而imageproxy的save()方法卻稍有不同,它會把建立好的image.image對象傳回給調用者,以備後續處理時所需。這樣修改應該不會出問題,因為假如調用者不使用傳回值的話(比如調用image.image.save()方法時,我們就沒打算使用傳回值),那麼python就會将其直接丢棄。imageproxy2.py程式無須像這樣修改,因為它有個類型為image.image的image屬性可供通路,如果通路時圖像尚未建立,那麼會當場建立一份。
像本例這樣把指令存儲起來,可以為實作“執行-撤銷”(do-undo)功能做準備,這一話題請參考3.2節的指令模式以及3.8節的狀态模式。
結構型設計模式都可以用python語言實作出來。擴充卡模式與外觀模式能夠把已有的類放在新環境下重新使用,而橋接模式則可以把某個類裡的複雜功能嵌入另一個類中。組合模式可以非常友善地建立出對象層次結構,但python中卻很少用到它,因為采用dict就可以實作相同的功能了。修飾器模式特别有用,python語言對此提供了原生支援,我們還可以用修飾器來修飾類。python的對象引用機制可以視為享元模式的變種。代理模式在python中實作起來非常簡單。設計模式不僅可用于建立各種簡單及複雜的對象,而且還能指導對象的行為,也就是規定單個對象或一組對象應該怎樣完成其工作。下一章就要講解這些“行為型設計模式”。