官术网_书友最值得收藏!

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文件,或者自己創建一個并嘗試一下。

主站蜘蛛池模板: 县级市| 清原| 汉川市| 周口市| 水城县| 邮箱| 文成县| 新建县| 龙山县| 确山县| 冀州市| 通渭县| 八宿县| 德保县| 重庆市| 钟祥市| 外汇| 黔东| 盐池县| 西城区| 昭苏县| 丘北县| 旌德县| 嘉定区| 平泉县| 夏津县| 寿阳县| 昌宁县| 山东省| 柞水县| 科技| 岱山县| 宜川县| 北安市| 花垣县| 云南省| 太谷县| 霍山县| 修文县| 新田县| 唐海县|