python 實現 工廠模式

本文目錄地址

本文代碼地址

創建型設計模式處理對象創建相關的問題,目標是當直接創建對象(在Python中是通過__init__()函數實現的)不太方便時,提供更好的方式。
在工廠設計模式中,客戶端①可以請求一個對象,而無需知道這個對象來自哪裏;也就是,使用哪個類來生成這個對象。工廠背後的思想是簡化對象的創建。與客戶端自己基於類實例化直接創建對象相比,基於一箇中心化函數來實現,更易於追蹤創建了哪些對象。通過將創建對象的代碼和使用對象的代碼解耦,工廠能夠降低應用維護的複雜度。
工廠通常有兩種形式:一種是工廠方法(Factory Method),它是一個方法(或以地道的Python術語來說,是一個函數),對不同的輸入參數返回不同的對象;第二種是抽象工廠,它是一組用於創建一系列相關事物對象的工廠方法。

目錄

1.1 工廠方法

1.1.1應用案例

1.1.2實現

1.2抽象工廠

1.2.1現實生活的例子

1.2.2應用案例

1.2.3實現

1.3小結


1.1 工廠方法

在工廠方法模式中,我們執行單個函數,傳入一個參數(提供信息表明我們想要什麼),但並不要求知道任何關於對象如何實現以及對象來自哪裏的細節。

1.1.1應用案例

如果因爲應用創建對象的代碼分佈在多個不同的地方,而不是僅在一個函數/方法中,你發現沒法跟蹤這些對象,那麼應該考慮使用工廠方法模式。工廠方法集中地在一個地方創建對象,使對象跟蹤變得更容易。注意,創建多個工廠方法也完全沒有問題,實踐中通常也這麼做,對相似的對象創建進行邏輯分組,每個工廠方法負責一個分組。例如,有一個工廠方法負責連接到不同的數據庫(MySQL、SQLite),另一個工廠方法負責創建要求的幾何對象(圓形、三角形),等等。
若需要將對象的創建和使用解耦,工廠方法也能派上用場。創建對象時,我們並沒有與某個特定類耦合/綁定到一起,而只是通過調用某個函數來提供關於我們想要什麼的部分信息。這意味着修改這個函數比較容易,不需要同時修改使用這個函數的代碼。
另外一個值得一提的應用案例與應用性能及內存使用相關。工廠方法可以在必要時創建新的對象,從而提高性能和內存使用率。若直接實例化類來創建對象,那麼每次創建新對象就需要分配額外的內存(除非這個類內部使用了緩存,一般情況下不會這樣)。用行動說話,下面的代碼(文件id.py)對同一個類A創建了兩個實例,並使用函數id()比較它們的內存地址。輸出中也會包含地址,便於檢查地址是否正確。內存地址不同就意味着創建了兩個不同的對象。

class A():
    pass

a=A()
b=A()

print(id(a)==id(b))
print(a,b)

輸出

False
<__main__.A object at 0x7fbeefe5ef98> <__main__.A object at 0x7fbeefe5efd0>

注意,你執行這個代碼文件看到的地址會與我看到的不一樣,因爲這依賴程序運行時內存的佈局和分配。但結果中有一點肯定是一樣的,那就是兩個地址不同。在Python Read-Eval-Print Loop(REPL)模式(即交互式提示模式)下編寫運行這段代碼時會出現例外,但這只是交互模式特有的優化,並不常見。

1.1.2實現

數據來源可以有多種形式。存取數據的文件主要有兩種分類:人類可讀文件和二進制文件。人類可讀文件的例子有:XML、Atom、YAML和JSON。二進制文件的例子則有SQLite使用的.sq3文件格式,及用於聽音樂的.mp3文件格式。
以下例子將關注兩種流行的人類可讀文件格式:XML和JSON。雖然人類可讀文件解析起來通常比二進制文件更慢,但更易於數據交換、審查和修改。基於這種考慮,建議優先使用人類可讀文件,除非有其他限制因素不允許使用這類格式(主要的限制包括性能不可接受以及專有的二進制格式)。
在當前這個問題中,我們有一些輸入數據存儲在一個XML文件和一個JSON文件中,要對這兩個文件進行解析,獲取一些信息。同時,希望能夠對這些(以及將來涉及的所有)外部服務進行集中式的客戶端連接。我們使用工廠方法來解決這個問題。雖然僅以XML和JSON爲例,但爲更多的服務添加支持也很簡單。

首先,來看一看數據文件。基於Wikipedia例子的XML文件person.xml包含個人信息(firstName、lastName、gender等),如下所示。

<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>

JSON文件donut.json來自Adobe的GitHub賬號,包含甜甜圈(donut)信息(type、單位價格ppu、topping等),如下所示。

[
    {
        "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.etree.ElementTree和json)來處理XML和JSON,如下所示。

import xml.etree.ElementTree as etree
import json

類JSONConnector解析JSON文件,通過parsed_data()方法以一個字典(dict)的形式返回數據。修飾器property使parsed_data()顯得更像一個常規的變量,而不是一個方法。類XMLConnector解析 XML 文件,通過parsed_data()方法以xml.etree.Element列表的形式返回所有數據,如下所示。

class JSONConnector:
    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
    
class XMLConnector:
    def __init__(self,filepath):
        self.tree=etree.parse(filepath)
        
    @property
    def parsed_data(self):
        return self.tree

函數connection_factory是一個工廠方法,基於輸入文件路徑的擴展名返回一個JSONConnector或XMLConnector的實例。

函數connect_to()對connection_factory()進行包裝,添加了異常處理,如下所示。

def connector_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=connector_factory(filepath)
    except ValueError as ve:
        print(ve)
    return factory

函數main()演示如何使用工廠方法設計模式。第一部分是確認異常處理是否有效。如下所示

def main():
    sqlite_factory=connect_to('data/person.sq3')

接下來的部分演示如何使用工廠方法處理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: {} dnouts'.format(len(json_data)))
    for donut in  json_data:
        print('name: {}'.format(donut['name']))
        print('price: ${}'.format(donut['ppu']))
        [print('topings: \t{}\t{}'.format(t['id'],t['type']))
         for t in donut['topping']]

爲便於整體理解,下面給出工廠方法實現(factory_method.py)的完整代碼。

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:
            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 connector_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 = connector_factory(filepath)
    except ValueError as ve:
        print(ve)
    return factory


def main():
    sqlite_factory = connect_to('data/person.sq3')

    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_factory = connect_to('data/donut.json')
    json_data = json_factory.parsed_data
    print('found: {} dnouts'.format(len(json_data)))
    for donut in json_data:
        print('name: {}'.format(donut['name']))
        print('price: ${}'.format(donut['ppu']))
        [print('topings: \t{}\t{}'.format(t['id'], t['type']))
         for t in donut['topping']]


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 dnouts
name: Cake
price: $0.55
topings: 	5001	None
topings: 	5002	Glazed
topings: 	5005	Sugar
topings: 	5007	Powdered Sugar
topings: 	5006	Chocolate with Sprinkles
topings: 	5003	Chocolate
topings: 	5004	Maple
name: Raised
price: $0.55
topings: 	5001	None
topings: 	5002	Glazed
topings: 	5005	Sugar
topings: 	5003	Chocolate
topings: 	5004	Maple
name: Old Fashioned
price: $0.55
topings: 	5001	None
topings: 	5002	Glazed
topings: 	5003	Chocolate
topings: 	5004	Maple

注意,雖然JSONConnector和XMLConnector擁有相同的接口,但是對於parsed_data()返回的數據並不是以統一的方式進行處理。對於每個連接器,需使用不同的Python代碼來處理。若能對所有連接器應用相同的代碼當然最好,但是在多數時候這是不現實的,除非對數據使用某種共同的映射,這種映射通常是由外部數據提供者提供。即使假設可以使用相同的代碼來處理XML和JSON文件,當需要支持第三種格式(例如,SQLite)時,又該對代碼作哪些改變呢?找一個SQlite文件或者自己創建一個,嘗試一下。

像現在這樣,代碼並未禁止直接實例化一個連接器。如果要禁止直接實例化,是否可以實現?試試看。

提示:Python中的函數可以內嵌類。

1.2抽象工廠

抽象工廠設計模式是抽象方法的一種泛化。概括來說,一個抽象工廠是(邏輯上的)一組工廠方法,其中的每個工廠方法負責產生不同種類的對象。

1.2.1現實生活的例子

汽車製造業應用了抽象工廠的思想。衝壓不同汽車模型的部件(車門、儀表盤、車篷、擋泥板及反光鏡等)所使用的機件是相同的。機件裝配起來的模型隨時可配置,且易於改變。從下圖我們能看到汽車製造業抽象工廠的一個例子,該圖由www.sourcemaking.com提供。

1.2.2應用案例

因爲抽象工廠模式是工廠方法模式的一種泛化,所以它能提供相同的好處:讓對象的創建更容易追蹤;將對象創建與使用解耦;提供優化內存佔用和應用性能的潛力。
這樣會產生一個問題:我們怎麼知道何時該使用工廠方法,何時又該使用抽象工廠?答案是,通常一開始時使用工廠方法,因爲它更簡單。如果後來發現應用需要許多工廠方法,那麼將創建一系列對象的過程合併在一起更合理,從而最終引入抽象工廠。
抽象工廠有一個優點,在使用工廠方法時從用戶視角通常是看不到的,那就是抽象工廠能夠通過改變激活的工廠方法動態地(運行時)改變應用行爲。一個經典例子是能夠讓用戶在使用應用時改變應用的觀感(比如,Apple風格和Windows風格等),而不需要終止應用然後重新啓動。

1.2.3實現

爲演示抽象工廠模式。想象一下,我們正在創造一個遊戲,或者想在應用中包含一個迷你遊戲讓用戶娛樂娛樂。我們希望至少包含兩個遊戲,一個面向孩子,一個面向成人。在運行時,基於用戶輸入,決定該創建哪個遊戲並運行。遊戲的創建部分由一個抽象工廠維護。
從孩子的遊戲說起,我們將該遊戲命名爲FrogWorld。主人公是一隻青蛙,喜歡吃蟲子。每個英雄都需要一個好名字, 在我們的例子中, 這個名字在運行時由用戶給定。方法interact_with()用於描述青蛙與障礙物(比如,蟲子、迷宮或其他青蛙)之間的交互,如下所示。

障礙物可以有多種,但對於我們的例子,可以僅僅是蟲子。當青蛙遇到一隻蟲子,只支持一種動作,那就是吃掉它!

class Frog:
    def __init__(self,name):
        self.name=name
        
    def __str__(self):
        return self.name
    
    def interact_with(self,obstacle):
        print('{} the Frog encounters {} and {}!'.format(self,obstacle,obstacle.action()))
        
class Bug:
    def __init__(self):
        return 'a bug'
    def action(self):
        return "eats it"

類FrogWorld是一個抽象工廠,其主要職責是創建遊戲的主人公和障礙物。區分創建方法並使其名字通用(比如,make_character()和make_obstacle()),這讓我們可以動態改變當前激活的工廠(也因此改變了當前激活的遊戲),而無需進行任何代碼變更。在一門靜態語言中,抽象工廠是一個抽象類/接口,具備一些空方法,但在Python中無需如此,因爲類型是在運行時檢測的,如下所示

class FrogWorld:
    def __init__(self,name):
        print(self)
        self.player_name=name
        
    def __str__(self):
        return '\n\n\t----------Frog World-----------'
    def make_character(self):
        return  Frog(self.player_name)
    def make_obstacle(self):
        return Bug()

WizardWorld遊戲也類似。在故事中唯一的區別是男巫戰怪獸(如獸人)而不是吃蟲子!

class Wizard:
    def __init__(self,name):
        self.name=name
        
    def __str__(self):
        return self.name
    def interact_with(self,obstacle):
        print('{} the Wizard battles against {} and {}!'.format(self,obstacle,obstacle.action()))
        
class Ork:
    def __str__(self):
        return 'an evil ork'
    def action(self):
        return 'kills it'
class WizardWorld:
    def __init__(self,name):
        print(self)
        self.player_name=name
    def __str__(self):
        return '\n\n\t-------------Wizard World---------------'

    def make_character(self):
        return Wizard(self.player_name)

    def make_obstacle(self):
        return Ork()

類GameEnvironment是我們遊戲的主入口。它接受factory作爲輸入,用其創建遊戲的世界。方法play()則會啓動hero和obstacle之間的交互,如下所示。

class GameEnvironment:
    def __init__(self,factory):
        self.hero=factory.make_character()
        self.obstacle=factory.make_obstacle()
    def play(self):
        self.hero.interact_with(self.obstacle)

函數validate_age()提示用戶提供一個有效的年齡。如果年齡無效,則會返回一個元組,其第一個元素設置爲False。如果年齡沒問題,元素的第一個元素則設置爲True,但我們真正關心的是元素的第二個元素,也就是用戶提供的年齡,如下所示。

def validate_age(name):
    try:
        age=input('Welcome {}. How old are you?'.format(name))
        age=int(age)
    except ValueError as err:
        print("Age {} is invalid, please try again...".format(age))
        return (False,age)
    return (True,age)

最後一個要點是main()函數,該函數請求用戶的姓名和年齡,並根據用戶的年齡決定該玩哪個遊戲,如下所示。

def main():
    name=input("hello, What's your name?")
    valid_input=False
    while not valid_input:
        valid_input, age=validate_age(name)
    game=FrogWorld if age<18 else WizardWorld
    environment=GameEnvironment(game(name))
    environment.play()

抽象工廠實現的完整代碼(abstract_factory.py)如下所示。

class Frog:
    def __init__(self, name):
        self.name = name

    def __str__(self):
        return self.name

    def interact_with(self, obstacle):
        print('{} the Frog encounters {} and {}!'.format(self, obstacle, obstacle.action()))


class Bug:
    def __init__(self):
        return 'a bug'

    def action(self):
        return "eats it"


class FrogWorld:
    def __init__(self, name):
        print(self)
        self.player_name = name

    def __str__(self):
        return '\n\n\t----------Frog World-----------'

    def make_character(self):
        return Frog(self.player_name)

    def make_obstacle(self):
        return Bug()


class Wizard:
    def __init__(self, name):
        self.name = name

    def __str__(self):
        return self.name

    def interact_with(self, obstacle):
        print('{} the Wizard battles against {} and {}!'.format(self, obstacle, obstacle.action()))


class Ork:
    def __str__(self):
        return 'an evil ork'

    def action(self):
        return 'kills it'


class WizardWorld:
    def __init__(self, name):
        print(self)
        self.player_name = name

    def __str__(self):
        return '\n\n\t-------------Wizard World---------------'

    def make_character(self):
        return Wizard(self.player_name)

    def make_obstacle(self):
        return Ork()


class GameEnvironment:
    def __init__(self, factory):
        self.hero = factory.make_character()
        self.obstacle = factory.make_obstacle()

    def play(self):
        self.hero.interact_with(self.obstacle)


def validate_age(name):
    try:
        age = input('Welcome {}. How old are you?'.format(name))
        age = int(age)
    except ValueError as err:
        print("Age {} is invalid, please try again...".format(age))
        return (False, age)
    return (True, age)


def main():
    name = input("hello, What's your name?")
    valid_input = False
    while not valid_input:
        valid_input, age = validate_age(name)
    game = FrogWorld if age < 18 else WizardWorld
    environment = GameEnvironment(game(name))
    environment.play()


main()

輸出

hello, What's your name?hbu
Welcome hbu. How old are you?56


	-------------Wizard World---------------
hbu the Wizard battles against an evil ork and kills it!

來嘗試擴展一下這個遊戲使其更完整吧。你可以隨意添加障礙物、敵人以及其他任何想要的東西。

1.3小結

我們學習瞭如何使用工廠方法和抽象工廠設計模式。兩種模式都可以用於以下幾種場景:(a)想要追蹤對象的創建時,(b)想要將對象的創建與使用解耦時,(c)想要優化應用的性能和資源佔用時。場景(c)並未詳細說明,你也許可以將其作爲一個練習。
工廠方法設計模式的實現是一個不屬於任何類的單一函數,負責單一種類對象(一個形狀、一個連接點或者其他對象)的創建。作爲示例,我們實現了一個工廠方法,提供了訪問XML和JSON文件的能力。
抽象工廠設計模式的實現是同屬於單個類的許多個工廠方法用於創建一系列種類的相關對象(一輛車的部件、一個遊戲的環境,或者其他對象)。我們提到抽象工廠如何與汽車製造業相關聯,並學習了抽象工廠的應用案例。作爲抽象工廠實現的示例,我們完成了一個迷你遊戲,演示瞭如何在單個類中使用多個相關工廠。
接下來我們將談論建造者模式,它是另一種創建型模式,可用於細粒度控制複雜對象的創建過程。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章