python設計模式-工廠方法模式

goodspeed發表於2019-02-20

題目:假設你有一個 pizza 店,功能包括下訂單做 pizza,你的程式碼會如何寫呢?

def order_pizza():
    pizza = Pizza()
    pizza.prepare()
    pizza.bake()
    pizza.cut()
    pizza.box()
    return pizza複製程式碼

但是現在你遇到了一個問題,你的 pizza 店需要更多的 pizza,所以現在你需要增加一些程式碼,來決定適合的 pizza 型別,然後再製造這個 pizza:

def order_pizza(pizza_type):  # 現在把 pizza 的型別傳入 order_pizza()

    # 根據 pizza 型別,我們例項化正確的具體類,然後將其賦值給 pizza 例項變數
    if pizza_type == `cheese`:
        pizza = CheesePizza()
    elif pizza_type == `greek`:
        pizza = GreekPizza()
    elif pizza_type == `pepperoni`:
        pizza = PepperoniPizza()
    # 一旦我們有了一個 pizza,需要做一些準備(擀麵皮、加佐料),然後烘烤、切片、裝盒
    pizza.prepare()
    pizza.bake()
    pizza.cut()
    pizza.box()
    return pizza複製程式碼

但是經過幾天的實踐,你發現顧客喜歡點的 ClamPizza、Veggie Pizza 而 Greek pizza 並沒有什麼人喜歡,這個時候需要修改程式碼:

def order_pizza(pizza_type):  # 現在把 pizza 的型別傳入 order_pizza()

    # 根據 pizza 型別,我們例項化正確的具體類,然後將其賦值給 pizza 例項變數
    if pizza_type == `cheese`:
        pizza = CheesePizza()
    # elif pizza_type == `greek`:  # greek pizza 不再出現在選單
    #     pizza = GreekPizza()
    elif pizza_type == `pepperoni`:
        pizza = PepperoniPizza()
    # 新加了 clam pizza 和 veggie pizza
    elif pizza_type == `clam`:
        pizza = ClamPizza()
    elif pizza_type == `veggie`:
        pizza = VeggiePizza()

    #  一旦我們有了一個 pizza,需要做一些準備(擀麵皮、加佐料),然後烘烤、切片、裝盒
    pizza.prepare()
    pizza.bake()
    pizza.cut()
    pizza.box()
    return pizza複製程式碼

現在你發現了一個問題, order_pizza() 是在內部例項化了具體的 Pizza 類,並且,order_pizza() 也沒有對修改關閉,以至於每次有了新的 pizza 加入都要修改 order_pizza() 的程式碼。這時一個比較好的辦法是把建立 Pizza 物件是抽象出來,修改後的程式碼如下:

# 把建立物件的程式碼從 order_pizza 方法中抽離
def create_pizza(pizza_type):
    # 根據 pizza 型別,我們例項化正確的具體類,然後將其賦值給 pizza 例項變數
    if pizza_type == `cheese`:
        pizza = CheesePizza()
    elif pizza_type == `pepperoni`:
        pizza = PepperoniPizza()
    elif pizza_type == `clam`:
        pizza = ClamPizza()
    elif pizza_type == `veggie`:
        pizza = VeggiePizza()
    return pizza

def order_pizza(pizza_type):  # 現在把 pizza 的型別傳入 order_pizza()
    # 這裡使用 create_pizza() 方法建立 pizza 類
    pizza = create_pizza(pizza_type)

    #  一旦我們有了一個 pizza,需要做一些準備(擀麵皮、加佐料),然後烘烤、切片、裝盒
    pizza.prepare()
    pizza.bake()
    pizza.cut()
    pizza.box()
    return pizza複製程式碼

簡單工廠模式

我們把建立 pizza 物件的程式碼提取到一個新的方法中,我們稱這個新的方法叫做工廠

工廠處理建立物件的細節,一旦有了create_pizzaorder_pizza() 就成了此物件的客戶。當需要 pizza 時,只需要告訴工廠需要什麼型別的 pizza,讓它做一個即可。

現在 order_pizza() 方法只關心從工廠得到一個 pizza,這個 pizza 實現了 Pizza 的介面,所以它可以呼叫 prepare()(準備)bake()(烘烤)cut()(切片)box()(裝盒)

問:現在你可能會問,這段程式碼看上去更復雜了,有什麼好處了呢?看上去只是把問題搬到另一個物件了。
答: 現在看來,order_pizza 只是create_pizza 的一個客戶,其它客戶(比如pizza 店選單 PizzaShopMenu)也可以使用這個工廠來取得 pizza。把建立 pizza 的程式碼包裝進一個類,當以後實現修改時,只需要修改這個部分程式碼即可。

這裡我們的工廠create_order() 是一個簡單的方法,利用方法定義一個簡單工廠的方法通常被稱為簡單工廠模式簡單工廠更像是一中程式設計習慣而不是設計模式)。

重做 PizzaStore 類

上邊的程式碼中,order_pizza 是客戶程式碼,但是為了讓我們的 pizza 店有更好的擴充套件性,這裡我們需要把客戶程式碼做一下修改:

class SimplePizzaFactory:

    def create_pizza(self, pizza_type):
        ...
        return pizza

class PizzaStore:
    def order_pizza(self, pizza_type):  # 現在把 pizza 的型別傳入 order_pizza()
        factory = SimplePizzaFactory()
        pizza = factory.create_pizza(pizza_type)
        ...
        return pizza

    # 下邊是其他可能用到的方法複製程式碼

這段程式碼中,我們把一個方法(create_pizza)使用類(SimplePizzaFactory)封裝了起來,目的是使工廠可以通過繼承來改變建立方法的行為,並且這樣做,也可以提高工廠方法的擴充套件性。

現在來看一下我們 pizza 店的類圖:

pizza店類圖
pizza店類圖

簡單工廠模式的侷限

缺點

  • 由於工廠類集中了所有產品建立邏輯,違反了高內聚責任分配原則,一旦不能正常工作,整個系統都要受到影響。
  • 系統擴充套件困難,一旦新增新產品就不得不修改工廠邏輯,在產品型別較多時,有可能造成工廠邏輯過於複雜,不利於系統的擴充套件和維護。

使用場景

  • 工廠類負責建立的物件較少
  • 客戶只知道傳入工廠類的引數,對於如何建立物件(邏輯)不關心;客戶端既不需要關心建立細節,甚至連類名都不需要記住,只需要知道型別所對應的引數。

為了突破這些侷限,我們接著看一下工廠方法模式

工廠方法模式

現在我們有了一個新的問題,我們建立 pizza 店後,現在有人想要加盟,但我們還想要控制一下 pizza 的製作流程,該如何實現呢?

首先,要給 pizza 店使用框架,我們所要做的就是把create_pizza()方法放回到PizzaStore類中,不過這個方法需要在每個子類中倒要實現一次。現在 PizzaStore程式碼為:

class PizzaStore:

    def create_pizza(self, pizza_type):
        # 每個需要子類實現的方法都會丟擲NotImplementedError
        # 我們也可以把 PizzaStore 的 metaclass 設定成 abc.ABCMeta
        # 這樣的話,這個類就是真正的抽象基類
        raise NotImplementedError()

    def order_pizza(self, pizza_type):  # 現在把 pizza 的型別傳入 order_pizza()

        pizza = self.create_pizza(pizza_type)

        #  一旦我們有了一個 pizza,需要做一些準備(擀麵皮、加佐料),然後烘烤、切片、裝盒
        pizza.prepare()
        pizza.bake()
        pizza.cut()
        pizza.box()
        return pizza複製程式碼

這樣我們就宣告瞭一個工廠方法。這個工廠方法用來處理物件的建立,並將這個建立行為封裝在子類中,這樣客戶程式中關於父類的程式碼就和子類的物件建立程式碼解耦成功。

我們將 create_pizza 放回 PizzaStore 的目的是讓繼承此方法的子類負責定義自己的create_pizza() 方法。現在我們看一下PizzaStore 的子類示意圖:

這裡 NYStlyePizzaStoreChicagoStylePizzaStore 需要分別定義自己的 create_pizza 方法。

現在來看下完整程式碼:

#! -*- coding: utf-8 -*-

class Pizza:

    name = None
    dough = None
    sauce = None
    toppings = []

    def prepare(self):
        print("Preparing %s" % self.name)
        print("Tossing dough...")
        print("Adding sauce...")
        print("Adding toppings: ")
        for topping in self.toppings:
            print("  %s" % topping)

    def bake(self):
        print("Bake for 25 minutes at 350")

    def cut(self):
        print("Cutting the pizza into diagonal slices")

    def box(self):
        print("Place pizza in official PizzaStore box")

    def __str__(self):
        return self.name


class NYStyleCheesePizza(Pizza):

    name = "NY Style Sauce and Cheese Pizza"
    dough = "Thin Crust Dough"
    sauce = "Marinara Sauce"
    toppings = ["Grated", "Reggiano", "Cheese"]


class ChicagoStyleCheesePizza(Pizza):

    name = "Chicago Style Deep Dish Cheese Pizza"
    dough = "Extra Thick Crust Dough"
    sauce = "Plum Tomato Sauce"
    toppings = ["Shredded", "Mozzarella", "Cheese"]

    def cut(self):
        print("Cutting the pizza into square slices")


class PizzaStore:
    def create_pizza(self, pizza_type):
        # 每個需要子類實現的方法都會丟擲NotImplementedError
        # 我們也可以把 PizzaStore 的 metaclass 設定成 abc.ABCMeta
        # 這樣的話,這個類就是真正的抽象基類
        raise NotImplementedError()

    def order_pizza(self, pizza_type):  # 現在把 pizza 的型別傳入 order_pizza()

        pizza = self.create_pizza(pizza_type)

        #  一旦我們有了一個 pizza,需要做一些準備(擀麵皮、加佐料),然後烘烤、切片、裝盒
        pizza.prepare()
        pizza.bake()
        pizza.cut()
        pizza.box()
        return pizza

class NYStylePizzStore(PizzaStore):
    def create_pizza(self, pizza_type):
        # 根據 pizza 型別,我們例項化正確的具體類,然後將其賦值給 pizza 例項變數
        if pizza_type == `cheese`:
            pizza = NYStyleCheesePizza()
        return pizza


class ChicagoStylePizzaStore(PizzaStore):
    def create_pizza(self, pizza_type):
        # 根據 pizza 型別,我們例項化正確的具體類,然後將其賦值給 pizza 例項變數
        if pizza_type == `cheese`:
            pizza = ChicagoStyleCheesePizza()
        return pizza


def main():
    nystore = NYStylePizzStore()
    pizza = nystore.order_pizza(`cheese`)
    print("goodspeed ordered a %s" % pizza)

    print("*" * 100)
    chicago_store = ChicagoStylePizzaStore()
    pizza = chicago_store.order_pizza(`cheese`)
    print("goodspeed ordered a %s" % pizza)


if __name__ == `__main__`:
    main()複製程式碼

這裡工廠方法 create_pizza() 直接丟擲了NotImplementedError,這樣做事為了強制子類重新實現 create_pizza() 方法,如果不重新實現就會丟擲NotImplementedError
當然也可以把 PizzaStore 的 metaclass 設定成 abc.ABCMeta 這樣的話,這個類就是真正的抽象基類。

現在我們看一下工廠方法模式的類圖:

工廠方法模式類圖
工廠方法模式類圖

產品類和建立者類其實是平行的類的層級它們的關係如下圖:

工廠方法模式定義

通過上文的介紹,我們可以得到工廠方法模式大概的定義:

工廠方法模式中,工廠父類負責定義建立產品物件的公共介面,而工廠子類則負責生成具體的產品物件,這樣做的目的是將產品類的例項化操作延遲到工廠子類中完成,即通過工廠子類來確定究竟應該例項化哪一個具體產品類。

工廠方法模式能夠封裝具體型別的例項化,抽象的 Creator 提供了一個建立物件的工廠方法。在抽象的 Creator 中,任何其他實現的方法,都可能使用到這個方法鎖製造出來的產品,但只有子類真正實現這個工廠方法並建立產品。

下圖是工廠方法模式原理類圖:

工廠方法模式類圖
工廠方法模式類圖

工廠方法模式優點

  • 工廠方法集中的在一個地方建立物件,使物件的跟蹤變得更容易。
  • 工廠方法模式可以幫助我們將產品的實現從使用解耦。如果增加產品或者改變產品的實現,Creator 並不會收到影響。
  • 使用工廠方法模式的另一個優點是在系統中加入新產品時,無須修改抽象工廠和抽象產品提供的介面,無須修改客戶端,也無須修改其他的具體工廠和具體產品,而只要新增一個具體工廠和具體產品就可以了。這樣,系統的可擴充套件性也就變得非常好,完全符合“開閉原則”。

工廠方法可以在必要時建立新的物件,從而提高效能和記憶體使用率。若直接例項化類來建立物件,那麼每次建立新物件就需要分配額外的記憶體。

簡單工廠工廠方法之間的差異

簡單工廠把全部的事情在一個地方處理完了(create_pizza),而工廠方法是建立了一個框架,讓子類去決定如何實現。比如在工廠方法中,order_pizza() 方法提供了一般的框架用來建立 pizza,order_pizza() 方法依賴工廠方法建立具體類,並製造出實際的 pizza。而製造什麼樣的 pizza 是通過繼承 PizzaStore來實現的。 但 簡單工廠 只是把物件封裝起來,並不具備工廠方法的彈性。

python 應用中使用工廠模式的例子

Django 的 forms 模組使用工廠方法模式來建立表單欄位。WTForm 也使用到了工廠方法模式。sqlalchemy 中不同資料庫連線部分也用到了工廠方法模式。

總結

工廠方法模式的核心思想是定義一個用來建立物件的公共介面,由工廠而不是客戶來決定需要被例項化的類,它通常在構造系統整體框架時被用到。工廠方法模式看上去似乎比較簡單,但是內涵卻極其深刻,抽象、封裝、繼承、委託、多型等物件導向設計中的理論都得到了很好的體現,應用範圍非常廣泛。

參考


最後,感謝女朋友支援。

歡迎關注(April_Louisa) 請我喝芬達
歡迎關注
歡迎關注
請我喝芬達
請我喝芬達

相關文章