工廠模式
創建型設計模式處理對象創建相關的問題,目標是當直接創建對象(在Python中是通過init()函數實現的)不太方便時,提供更好的方式。
在工廠設計模式中,客戶端1可以請求一個對象,而無需知道這個對象來自哪里;也就是說,無需知道使用哪個類來生成這個對象。工廠背后的思想是簡化對象的創建。與客戶端自己基于類實例化直接創建對象相比,基于一個中心化函數來實現,也更易于追蹤創建了哪些對象。通過將創建對象的代碼和使用對象的代碼解耦,工廠能夠降低應用維護的復雜度。
工廠通常有兩種形式:一種是工廠方法(Factory Method),它是一個方法(或以地道的Python 術語來說,是一個函數),對不同的輸入參數返回不同的對象;第二種是抽象工廠,它是一組用于創建一系列相關事物對象的工廠方法。
工廠方法
在工廠方法模式中,我們執行單個函數,傳入一個參數(提供信息表明我們想要什么),但并不要求知道任何關于對象如何實現以及對象來自哪里的細節。
如果因為應用創建對象的代碼分布在多個不同的地方,而不是僅在一個函數/方法中,你發現沒法跟蹤這些對象,那么應該考慮使用工廠方法模式。工廠方法集中地在一個地方創建對象,使對象跟蹤變得更容易。注意,創建多個工廠方法也完全沒有問題,實踐中通常也這么做,對相似的對象創建進行邏輯分組,每個工廠方法負責一個分組。例如,有一個工廠方法負責連接到不同的數據庫(MySQL、SQLite),另一個工廠方法負責創建要求的幾何對象(圓形、三角形),等等。
若需要將對象的創建和使用解耦,工廠方法也能派上用場。創建對象時,我們并沒有與某個 12 特定類耦合/綁定到一起,而只是通過調用某個函數來提供關于我們想要什么的部分信息。這意味著修改這個函數比較容易,不需要同時修改使用這個函數的代碼。
另外一個值得一提的應用案例與應用性能及內存使用相關。工廠方法可以在必要時創建新的對象,從而提高性能和內存使用率。若直接實例化類來創建對象,那么每次創建新對象就需要分配額外的內存(除非這個類內部使用了緩存,一般情況下不會這樣)。用行動說話,下面的代碼(文件id.py)對同一個類A創建了兩個實例,并使用函數id()比較它們的內存地址。輸出中也會包含地址,便于檢查地址是否正確。內存地址不同就意味著創建了兩個不同的對象。
首先,讓我們先查看一下github上的代碼:
# 參考github上的代碼:
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""http://ginstrom.com/scribbles/2007/10/08/design-patterns-python-style/"""
class GreekGetter:
"""A simple localizer a la gettext"""
def __init__(self):
self.trans = dict(dog="σκ?λο?", cat="γ?τα")
def get(self, msgid):
"""We'll punt if we don't have a translation"""
return self.trans.get(msgid, str(msgid))
class EnglishGetter:
"""Simply echoes the msg ids"""
def get(self, msgid):
return str(msgid)
def get_localizer(language="English"):
"""The factory method"""
languages = dict(English=EnglishGetter, Greek=GreekGetter)
return languages[language]()
# Create our localizers
e, g = get_localizer(language="English"), get_localizer(language="Greek")
# Localize some text
for msgid in "dog parrot cat bear".split():
print(e.get(msgid), g.get(msgid))
### OUTPUT ###
# dog σκ?λο?
# parrot parrot
# cat γ?τα
# bear bear
通過一個函數工廠,返回得到相應的實例,所要傳入的是一個參數,其他都不需要去管理,函數自動找到需要實例化的類。
我們將使用Python發行版自帶的兩個庫(xml.etree.ElementTree和json)來處理 XML 和 JSON,如下所示:
import xml.etree.ElementTree as etree
import json
類JSONConnector解析JSON文件,通過parsed_data()方法以一個字典(dict)的形式 返回數據。修飾器property使parsed_data()顯得更像一個常規的變量,而不是一個方法,如下所示。
class JSONConnector:
def __init__(self, filepath):
self.data = dict()
# 比較奇怪的是這里會報錯
# with open(filepath, mode='r', encoding='utf-8') as f:
with open(filepath, mode='r') as f:
self.data = json.load(f)
@property
def parsed_data(self):
return self.data
類XMLConnector解析 XML 文件,通過parsed_data()方法以xml.etree.Element列表的形式返回所有數據,如下所示:
class XMLConnector:
def __init__(self, filepath):
self.tree = etree.parse(filepath)
@property
def parsed_data(self):
return self.tree
函數connection_factory是一個工廠方法,基于輸入文件路徑的擴展名返回一個JSONConnector或XMLConnector的實例,如下所示:
def connection_factory(filepath):
if filepath.endswith('json'):
connector = JSONConnector
elif filepath.endswith('xml'):
connector = XMLConnector
else:
raise ValueError('Cannot connect to {}'.format(filepath))
return connector(filepath)
函數connect_to()對connection_factory()進行包裝,添加了異常處理,如下所示:
def connect_to(filepath):
factory = None
try:
factory = connection_factory(filepath)
except ValueError as ve:
print(ve)
return factory
演示如何使用工廠方法處理XML文件。XPath用于查找所有包含姓(last name) 為Liar的person元素。對于每個匹配到的元素,展示其基本的姓名和電話號碼信息,如下所示。
xml_factory = connect_to('data/person.xml')
xml_data = xml_factory.parsed_data
liars = xml_data.findall(".//{}[{}='{}']".format('person', 'lastName', 'Liar'))
print('found: {} persons'.format(len(liars)))
for liar in liars:
print('first name: {}'.format(liar.find('firstName').text))
print('last name: {}'.format(liar.find('lastName').text))
[print('phone number ({})'.format(p.attrib['type']), p.text) for p in liar.find('phoneNumbers')]
最后一部分演示如何使用工廠方法處理JSON文件。這里沒有模式匹配,因此所有甜甜圈的 name、price和topping,如下所示。
json_factory = connect_to('data/donut.json')
json_data = json_factory.parsed_data
print('found: {} donuts'.format(len(json_data)))
for donut in json_data:
print('name: {}'.format(donut['name']))
print('price: ${}'.format(donut['ppu']))
[print('topping: {} {}'.format(t['id'], t['type'])) for t in donut['topping']]
完整代碼如下所示:
import xml.etree.ElementTree as etree
import json
class JSONConnector:
def __init__(self, filepath):
self.data = dict()
# 比較奇怪的是這里會報錯
# with open(filepath, mode='r', encoding='utf-8') as f:
with open(filepath, mode='r') as f:
self.data = json.load(f)
@property
def parsed_data(self):
return self.data
class XMLConnector:
def __init__(self, filepath):
self.tree = etree.parse(filepath)
@property
def parsed_data(self):
return self.tree
def connection_factory(filepath):
if filepath.endswith('json'):
connector = JSONConnector
elif filepath.endswith('xml'):
connector = XMLConnector
else:
raise ValueError('Cannot connect to {}'.format(filepath))
return connector(filepath)
def connect_to(filepath):
factory = None
try:
factory = connection_factory(filepath)
except ValueError as ve:
print(ve)
return factory
sqlite_factory = connect_to('data/person.sq3')
print()
xml_factory = connect_to('data/person.xml')
xml_data = xml_factory.parsed_data
liars = xml_data.findall(".//{}[{}='{}']".format('person', 'lastName', 'Liar'))
print('found: {} persons'.format(len(liars)))
for liar in liars:
print('first name: {}'.format(liar.find('firstName').text))
print('last name: {}'.format(liar.find('lastName').text))
[print('phone number ({})'.format(p.attrib['type']), p.text) for p in liar.find('phoneNumbers')]
print()
json_factory = connect_to('data/donut.json')
json_data = json_factory.parsed_data
print('found: {} donuts'.format(len(json_data)))
for donut in json_data:
print('name: {}'.format(donut['name']))
print('price: ${}'.format(donut['ppu']))
[print('topping: {} {}'.format(t['id'], t['type'])) for t in donut['topping']]
注意,雖然JSONConnector和XMLConnector擁有相同的接口,但是對于parsed_data() 返回的數據并不是以統一的方式進行處理。對于每個連接器,需使用不同的Python代碼來處理。 若能對所有連接器應用相同的代碼當然最好,但是在多數時候這是不現實的,除非對數據使用某 種共同的映射,這種映射通常是由外部數據提供者提供。即使假設可以使用相同的代碼來處理 XML和JSON文件,當需要支持第三種格式(例如,SQLite)時,又該對代碼作哪些改變呢?找一個SQlite文件或者自己創建一個,嘗試一下。
只需要通過connection_factory上加一個sqlite的文件格式,然后再添加一個對于sql操作的類就好。
代碼并未禁止直接實例化一個連接器。如果要禁止直接實例化,是否可以實現?
可以,Python中允許class中嵌套class。