Python設計模式之工廠模式

工廠模式

創建型設計模式處理對象創建相關的問題,目標是當直接創建對象(在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。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,763評論 6 539
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,238評論 3 428
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,823評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,604評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,339評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,713評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,712評論 3 445
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,893評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,448評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,201評論 3 357
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,397評論 1 372
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,944評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,631評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,033評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,321評論 1 293
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,128評論 3 398
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,347評論 2 377

推薦閱讀更多精彩內容

  • Spring Cloud為開發人員提供了快速構建分布式系統中一些常見模式的工具(例如配置管理,服務發現,斷路器,智...
    卡卡羅2017閱讀 134,818評論 18 139
  • 簡單工廠模式雖然簡單,但存在一個很嚴重的問題。當系統中需要引入新產品時,由于靜態工廠方法通過所傳入參數的不同來創建...
    justCode_閱讀 1,200評論 1 9
  • 1. Java基礎部分 基礎部分的順序:基本語法,類相關的語法,內部類的語法,繼承相關的語法,異常的語法,線程的語...
    子非魚_t_閱讀 31,733評論 18 399
  • 高校畢業總要面臨一個嚴峻的問題,該去怎樣的公司呢?籠統的說,是去大公司還是小公司呢?準確的說,每個人都有不同的情況...
    三丘的世界閱讀 205評論 0 2
  • 我想陪你走下去,不要車,不要房,現在你說我這么好,覺得對我不公平陪你過苦日子。可我擔心的不是過苦日子,我擔...
    雨涵清揚閱讀 460評論 1 0