什麼叫工廠方法模式
在工廠方法模式中,我們提供一個帶參數的函數,依據參數的不同,返回不同的實例對象。工廠方法模式並不需要知道創建實例對象的具體細節。例如,我們現在有不同的文件,XML、Atom、YAML和 JSON,我們需要解析這些文件,利用 工廠方法模式 如何實現呢?如下圖所示:
工廠方法模式的好處
工廠模式背後的思想是簡化對象的創建。
- 隱藏對象創建的細節: 在工廠設計模式中,客戶端可以請求一個對象,無需知道對象來自哪裏和創建細節;
- 易於追蹤創建的對象: 與客戶端自己基於類實例化直接創建對象相比,基於一箇中心化函數來實現,更易於追蹤創建了哪些對象;
- 使用與創建解耦: 可以實現創建對象的代碼與使用對象的代碼解耦,降低了代碼維護的複雜度;
- 優化性能以及內存: 工廠方法只有在實際需要的時候纔會創建實例對象,從而提高了性能和使用率;
工廠方法模式如何使用
假設我們要解析一堆文件,這些文件有 xml
、json
、yaml
等,我們利用工廠模式完成這一功能。
xml 和 json 文件的格式
我們存在兩個文件:person.xml
和 donut.json
文件,其中:
person.xml
:包含個人信息:firstName、lastName、age、address、phoneNumber、gender
;具體信息如下所示:
// data/person.xml
<persons>
<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>
<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>
<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>
</persons>
donut.json
:包含甜甜圈的信息:id、type、name、ppu、batters、topping
等,具體格式如下:
# data/donut.json
[
{
"id": "0001",
"type": "donut",
"name": "Cake",
"ppu": 0.55,
"batters": {
"batter": [
{
"id": "1001",
"type": "Regular"
},
{
"id": "1002",
"type": "Chocolate"
},
{
"id": "1003",
"type": "Blueberry"
},
{
"id": "1004",
"type": "Devil's Food"
}
]
},
"topping": [
{
"id": "5001",
"type": "None"
},
{
"id": "5002",
"type": "Glazed"
},
{
"id": "5005",
"type": "Sugar"
},
{
"id": "5007",
"type": "Powdered Sugar"
},
{
"id": "5006",
"type": "Chocolate with Sprinkles"
},
{
"id": "5003",
"type": "Chocolate"
},
{
"id": "5004",
"type": "Maple"
}
]
},
{
"id": "0002",
"type": "donut",
"name": "Raised",
"ppu": 0.55,
"batters": {
"batter": [
{
"id": "1001",
"type": "Regular"
}
]
},
"topping": [
{
"id": "5001",
"type": "None"
},
{
"id": "5002",
"type": "Glazed"
},
{
"id": "5005",
"type": "Sugar"
},
{
"id": "5003",
"type": "Chocolate"
},
{
"id": "5004",
"type": "Maple"
}
]
},
{
"id": "0003",
"type": "donut",
"name": "Old Fashioned",
"ppu": 0.55,
"batters": {
"batter": [
{
"id": "1001",
"type": "Regular"
},
{
"id": "1002",
"type": "Chocolate"
}
]
},
"topping": [
{
"id": "5001",
"type": "None"
},
{
"id": "5002",
"type": "Glazed"
},
{
"id": "5003",
"type": "Chocolate"
},
{
"id": "5004",
"type": "Maple"
}
]
}
]
涉及到的頭文件
我們將使用Python發行版自帶的兩個庫來處理XML和JSON,如下所示:
import xml.etree.ElementTree as etree
import json
相關的類
爲每一個類型的文件建立一個解析類:
JSONConnector
用來解析 JSON 文件;XMLConnector
用來解析 XML 文件;
# 處理JSON文件類
class JSONConnector(object):
def __init__(self, filepath):
self.data = dict()
with open(filepath, mode='r', encoding='utf-8') as file:
self.data = json.load(file)
@property
def parsed_data(self):
return self.data
# 處理XML文件的類
class XMLConnector(object):
def __init__(self, filepath):
self.tree = etree.parse(filepath)
@property
def parsed_data(self):
return self.tree
上述代碼中:
- 在 JSON 類中,通過
parsed_data()
方法以一個字典(dict)的形式返回數據; - 在 XML 類中,通過
parsed_data()
方法以xml.etree.Element
列表的形式返回所有數據; - 修飾器
property
使得parsed_data()
可以向常規的變量一樣使用;
建立工廠函數
工廠函數以文件路徑名爲參數,返回解析相應文件的實例對象,代碼如下所示:
# 工廠方法:基於文件擴展名, 返回一個JSONConnector或XMLConnector的實例
def factory_mode(filepath):
if filepath.endswith('json'):
connector = JSONConnector
elif filepath.endswith('xml'):
connector = XMLConnector
else:
raise ValueError('Cannot connect to {}'.format(filepath))
return connector(filepath)
# 對connector進行包裝
def connect_to(filepath):
factory = None
try:
factory = factory_mode(filepath)
except ValueError as ve:
print(ve)
return factory
上述代碼中,connect_to()
是對 factory_mode()
的包裝,添加了異常處理功能。
演示工廠方法模式
演示代碼如下所示:
def main():
# 確認異常處理是否有效
sqlite_factory = connect_to('data/person.sq3')
print()
# 解析xml文件
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文件
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']]
if __name__ == '__main__':
main()
結果如下所示:
Cannot connect to data/person.sq3
found: 2 persons
first name: Jimy
last name: Liar
phone number (home) 212 555-1234
first name: Patty
last name: Liar
phone number (home) 212 555-1234
phone number (mobile) 001 452-8819
found: 3 donuts
name: Cake
price: $0.55
topping: 5001 None
topping: 5002 Glazed
topping: 5005 Sugar
topping: 5007 Powdered Sugar
topping: 5006 Chocolate with Sprinkles
topping: 5003 Chocolate
topping: 5004 Maple
name: Raised
price: $0.55
topping: 5001 None
topping: 5002 Glazed
topping: 5005 Sugar
topping: 5003 Chocolate
topping: 5004 Maple
name: Old Fashioned
price: $0.55
topping: 5001 None
topping: 5002 Glazed
topping: 5003 Chocolate
topping: 5004 Maple
源碼鏈接在這裏;
參考資料
- <精通Python設計模式>;