- 精通Python設計模式(第2版)
- (法)卡蒙·阿耶娃 (荷)薩基斯·卡薩姆帕利斯
- 2784字
- 2020-04-22 12:23:51
1.1 工廠方法
工廠方法基于單一的函數來處理對象創建任務。執行工廠方法、傳入一個參數以提供體現意圖的信息,就可以創建想要的對象。
有趣的是,使用工廠方法并不要求知道對象的實現細節及其來源。
1.1.1 現實生活中的例子
現實生活中使用工廠方法的一個例子是塑料玩具制造。用于制造塑料玩具的材料都是相同的,但使用不同的塑料模具能生產出不同的玩具(不同形象或形狀)。這就像有一個工廠方法,輸入是所期望玩具的名稱(例如,鴨子或小車),輸出(成型后)則是所需的塑料玩具。
在軟件世界,Django框架使用工廠方法來創建表單字段。Django的forms模塊支持創建不同種類的字段(如CharField和EmailField等),其部分行為可以通過max_length或required(j.mp/djangofac)等屬性來定制。例如,在下面這段代碼中,開發者創建了一個表單(PersonForm表單包括name與birth_date字段)作為Django應用UI代碼的一部分:
from django import forms class PersonForm(forms.Form): name = forms.CharField(max_length=100) birth_date = forms.DateField(required=False)
1.1.2 用例
如果你發現,創建對象的代碼分布在許多不同的地方,而不是在單一的函數/方法中,導致難以跟蹤應用中創建的對象,這時就應該考慮使用工廠方法模式了。工廠方法將對象創建過程集中化,使得追蹤對象變得更容易。注意,創建多個工廠方法完全沒有問題,實踐中也通常這么做。每個工廠方法從邏輯上將具有相同點的對象的創建過程劃分為一組。例如,一個工廠方法負責連接到不同的數據庫(MySQL、SQLite),另一個工廠方法負責創建所要求的幾何對象(圓形、三角形),等等。
要將對象的創建與使用解耦,工廠方法也十分有用。創建對象時,我們沒有與某個特定的類耦合/綁定到一起,而只是通過調用某個函數來提供關于自身需求的部分信息。這意味著修改函數十分容易,而且不需要同時修改使用這個函數的代碼。
另一個值得一提的用例與提升應用程序的性能以及內存使用率有關。工廠方法僅在絕對必要時才創建新的對象,以提升性能與內存使用率。當直接通過實例化類來創建對象時,每次創建一個新的對象都會分配額外的內存(除非這個類使用了內部緩存,多數情況下并非如此)。我們能夠看到,在實踐中,下列代碼(id.py文件)創建了兩個都屬于類A的實例,并使用id()函數比較其內存地址。它們的地址都打印在輸出中以便我們觀察。內存地址不同意味著創建了兩個不同的對象。
class A: pass if __name__ == '__main__': a = A() b = A() print(id(a) == id(b)) print(a, b)
在我的計算機上執行python id.py命令,輸出了如下結果:
False <__main__.A object at 0x7f5771de8f60> <__main__.A object at 0x7f5771df2208>
注意,你執行該文件得到的地址與我的并不相同,因為它們依賴于實時的內存布局與分配。但結果必然相同——兩個地址應該不同。如果你在Python Read-Eval-Print-Loop(REPL,交互式解釋器,或簡單而言,交互式對話框)中編寫并執行代碼,可能會出現例外,但那只是一個REPL特有的優化,一般不會發生。
1.1.3 工廠方法的實現
數據通常以多種形式呈現。存取數據的文件類型主要有兩種:人類可讀文件與二進制文件。人類可讀文件的例子有XML、RSS/Atom、YAML和JSON等。二進制文件則包括SQLite使用的.sq3文件,以及用于聽音樂的.mp3音頻文件。
本案例將重點闡述兩種流行的人類可讀文件——XML和JSON。通常來說,雖然解析人類可讀文件要比解析二進制文件慢,但人類可讀文件能簡化數據交換、檢查與修改的過程。因此,建議你使用人類可讀文件,除非存在其他限制因素(主要包括不可接受的低性能與專有的二進制格式)。
本例中,我們有一些輸入數據,它們被存儲在一個XML文件和一個JSON文件之中。我們希望將其解析,并獲取一些信息。同時,我們希望集中客戶端與那些(以及未來所有的)外部服務的聯系。我們將使用工廠方法來解決這個問題。雖然本例只關注XML與JSON,但添加對其他服務的支持也十分簡單。
首先,觀察這個數據文件。
JSON文件movies.json是GitHub上的一個例子。它是一個包含美國電影信息的數據集合(如標題、年份、導演名、體裁,等等)。實際上,這是一個非常大的文件,這里呈現的只是它的摘錄。我們將其簡化以便閱讀,用于展示其文件結構。
[ {"title":"After Dark in Central Park", "year":1900, "director":null, "cast":null, "genre":null}, {"title":"Boarding School Girls' Pajama Parade", "year":1900, "director":null, "cast":null, "genre":null}, {"title":"Buffalo Bill's Wild West Parad", "year":1900, "director":null, "cast":null, "genre":null}, {"title":"Caught", "year":1900, "director":null, "cast":null, "genre":null}, {"title":"Clowns Spinning Hats", "year":1900, "director":null, "cast":null, "genre":null}, {"title":"Capture of Boer Battery by British", "year":1900, "director":"James H. White", "cast":null, "genre":"Short documentary"}, {"title":"The Enchanted Drawing", "year":1900, "director":"J. Stuart Blackton", "cast":null, "genre":null}, {"title":"Family Troubles", "year":1900, "director":null, "cast":null, "genre":null}, {"title":"Feeding Sea Lions", "year":1900, "director":null, "cast":"Paul Boyton", "genre":null} ]
XML文件person.xml基于維基百科上的一個例子(j.mp/wikijson)。它包含許多個人信息(如名、姓、性別等)。
(1) 我們以一個名為persons的XML容器的閉合標簽作為開始。
<persons>
(2) 展示一個人的數據的XML元素。
<person> <firstName>John</firstName> <lastName>Smith</lastName> <age>25</age> <address> <streetAddress>21 2nd Street</streetAddress> <city>New York</city> <state>NY</state> <postalCode>10021</postalCode> </address> <phoneNumbers> <phoneNumber type="home">212 555-1234</phoneNumber> <phoneNumber type="fax">646 555-4567</phoneNumber> </phoneNumbers> <gender> <type>male</type> </gender> </person>
(3) 展示另一個人數據的XML元素。
<person> <firstName>Jimy</firstName> <lastName>Liar</lastName> <age>19</age> <address> <streetAddress>18 2nd Street</streetAddress> <city>New York</city> <state>NY</state> <postalCode>10021</postalCode> </address> <phoneNumbers> <phoneNumber type="home">212 555-1234</phoneNumber> </phoneNumbers> <gender> <type>male</type> </gender> </person>
(4) 展示第三個人數據的XML元素。
<person> <firstName>Patty</firstName> <lastName>Liar</lastName> <age>20</age> <address> <streetAddress>18 2nd Street</streetAddress> <city>New York</city> <state>NY</state> <postalCode>10021</postalCode> </address> <phoneNumbers> <phoneNumber type="home">212 555-1234</phoneNumber> <phoneNumber type="mobile">001 452-8819</phoneNumber> </phoneNumbers> <gender> <type>female</type> </gender> </person>
(5) 最后,閉合這個XML容器。
</persons>
我們將使用兩個庫——json和xml.etree.ElementTree。它們是Python發行版的一部分,用于解析JSON與XML。
import json import xml.etree.ElementTree as etree
類JSONDataExtractor用于解析JSON文件,它有一個parsed_data()方法,返回一個包含所有數據的字典(dict)。裝飾器property用于使parsed_data()變得更像一個普通的屬性而非方法。代碼如下:
class JSONDataExtractor: def __init__(self, filepath): self.data = dict() with open(filepath, mode='r', encoding='utf-8') as f:self.data = json.load(f) @property def parsed_data(self): return self.data
類XMLDataExtractor用于解析XML文件。它有一個parsed_data()方法,返回由xml.etree.Element組成的、包含所有數據的列表。代碼如下:
class XMLDataExtractor: def __init__(self, filepath): self.tree = etree.parse(filepath) @property def parsed_data(self): return self.tree
函數dataextraction_factory()是一個工廠方法。它根據輸入文件的擴展名,返回一個JSONDataExtractor或XMLDataExtractor的實例。代碼如下:
def dataextraction_factory(filepath): if filepath.endswith('json'): extractor = JSONDataExtractor elif filepath.endswith('xml'): extractor = XMLDataExtractor else: raise ValueError('Cannot extract data from {}'.format(filepath)) return extractor(filepath)
函數extract_data_from()是dataextraction_factory()的一個裝飾器。它增加了異常處理機制。代碼如下:
def extract_data_from(filepath): factory_obj = None try: factory_obj = dataextraction_factory(filepath) except ValueError as e: print(e) return factory_obj
函數main()展示了如何使用工廠方法。第一部分確保了異常處理機制的有效性。代碼如下:
def main(): sqlite_factory = extract_data_from('data/person.sq3') print()
第二部分展示了如何使用工廠方法處理JSON文件。(在數據非空的情況下)該解析方法能夠展示電影的標題、年份、導演姓名以及體裁。代碼如下:
json_factory = extract_data_from('data/movies.json') json_data = json_factory.parsed_data print(f'Found: {len(json_data)} movies') for movie in json_data: print(f"Title: {movie['title']}") year = movie['year'] if year: print(f"Year: {year}") director = movie['director'] if director: print(f"Director: {director}") genre = movie['genre'] if genre: print(f"Genre: {genre}") print()
最后一部分展示了如何使用工廠方法處理XML文件。Xpath用于尋找所有姓為Liar的person元素(使用liars = xml_data.findall(f".//person[lastName='Liar']"))。對于每一個匹配的人,將展示其基本姓名與電話號碼信息:
xml_factory = extract_data_from('data/person.xml') xml_data = xml_factory.parsed_data liars = xml_data.findall(f".//person[lastName='Liar']") print(f'found: {len(liars)} persons') for liar in liars: firstname = liar.find('firstName').text print(f'first name: {firstname}') lastname = liar.find('lastName').text print(f'last name: {lastname}') [print(f"phone number ({p.attrib['type']}):", p.text) for p in liar.find('phoneNumbers')] print()
下面是一份代碼實現的總結(你可以在factory_method.py文件中找到代碼)。
(1) 導入所需的模塊(json和ElementTree)。
(2) 定義JSON數據提取器類(JSONDataExtractor)。
(3) 定義XML數據提取器類(XMLDataExtractor)。
(4) 添加工廠函數dataextraction_factory(),以獲得正確的數據提取器類。
(5) 添加處理異常的裝飾器函數extract_data_from()。
(6) 最終,添加main()函數,并使用Python傳統的命令行方式調用該函數。main函數的要點如下。
? 嘗試從SQL文件(data/person.sq3)中提取數據,以展示異常處理的方式。
? 從JSON文件中提取數據并解析出結果。
? 從XML文件中提取數據并解析出結果。
調用python factory_method.py命令將得到以下輸出(對于不同的輸入,輸出也不同)。
首先,當你試圖訪問一個SQLite(.sq3)文件時,會出現如下這樣一條異常消息。

其次,處理movies文件(JSON)時,你會獲得如下結果。

最后,處理person XML文件,并查找姓為Liar的人時,會出現如下結果。

注意,雖然JSONDataExtractor和XMLDataExtractor有相同的接口,但是它們處理parsed_data()返回值的方式并不一致。每個數據解析器必須與不同的Python代碼相配套。雖然能對所有的提取器使用相同的代碼聽起來很美妙,但這在大多數情況下并不符合實際,除非我們使用的數據具有相同的映射,而這些數據常常由外部數據供應商提供。假設你能使用相同的代碼來處理XML和JSON文件,那么又需要做何改變以支持第三種格式呢?SQLlite怎么樣?找一個SQLite文件,或者自己創建一個并嘗試一下。
- 軟件安全技術
- .NET之美:.NET關鍵技術深入解析
- Python科學計算(第2版)
- 數據庫程序員面試筆試真題與解析
- Spring Cloud Alibaba微服務架構設計與開發實戰
- 構建移動網站與APP:HTML 5移動開發入門與實戰(跨平臺移動開發叢書)
- 基于差分進化的優化方法及應用
- Scratch 3游戲與人工智能編程完全自學教程
- Processing互動編程藝術
- Mastering Linux Network Administration
- Advanced Express Web Application Development
- Natural Language Processing with Java and LingPipe Cookbook
- Solr Cookbook(Third Edition)
- Java編程從入門到精通
- 3ds Max印象 電視欄目包裝動畫與特效制作