利用 Abstract Factory Pattern - Design Patterns 學習設計模式 Abstract Factory Pattern,並利用 Python 撰寫 sample

摘要

簡而言之, Factory Method Pattern 描述的是定義製作者(Creator)和產品(Product)間的關係 , Abstract Factory Pattern 則是一種將多個製作者(Creator) 群組化的關係;

Abstract Factory Pattern 是基於 Factory Method Pattern 建構而成的一種設計模式,因此需要先理解 Factory Method Pattern 的核心精神與設計方式

Design Patterns - Factory Method Pattern
Date: 2023-03-08   Categories: #Design Patterns 
Factory Method Pattern 簡明扼要的說,就是定義製作者(Creator)和產品(Product)間的關係。我們並不需要在意具體是哪個製作者生產產品,也不需要在意製作者用何種方式生產特定產品,因為我們關注的部分為,是否可拿到特定產品。 ......

影片中提供的案例是應用程式的 UI 設計 :

當一組應用程式的 UI 控制項固定後,每個控制項功能背後要達成的目的是固定的,但依據作業系統的不同,如: Windows、Mac OS 、 Linux,控制項功能需要有不同的實作方式,如: 呼叫不同的 driver 或 kernel APIs,以達成目的,而 Factory Method Pattern 恰好適合解決這個問題;

因此,透過宣告 Abstract Factory Pattern,將每個控制項功能當作一種產品(Product),透過不同的製作者(Creator)來產生控制項,並將適用於同一種作業系統的製作者(Creator)整合成一個群組,當 需要渲染出符合當前作業系統的UI 時,只要採納對應的群組生成控制項功能就能達成目的,這也表達了 Abstract Factory Pattern 的設計理念


舉例

假設有一速食快餐店要推出漢堡系列的套餐,套餐的組合中有主餐漢堡、副餐炸薯條和一杯飲料;其中炸薯條和飲料可以點選不同的尺寸選擇,並且預設為中份薯條和大杯飲料。

在這個場景中,使用 Abstract Factory Pattern 來實現套餐組合的過程。

產品 (Product)

整個套餐是透過不同的產品組合而成,假設當前已知的產品如下表

產品類型產品名稱單價單位
肉 (Meat)牛肉 (Beef)30.0
肉 (Meat)豬肉 (Pork)25.0
肉 (Meat)魚肉 (Fish)20.0
飲料 (Drink)可樂 (Coke)依據尺寸
飲料 (Drink)蘇打 (Soda)依據尺寸
飲料 (Drink)綠茶 (GreenTea)依據尺寸
薯條 (Fries)薯條 (Fries)依據尺寸
漢堡 (Burger)漢堡 (Burger)依據肉量 + 20.0

從產品表中可以整理出下列資訊 :

  • 漢堡 : 漢堡由麵包和肉類組成。依據加入的肉類與肉片量,有不同的售價。
  • 飲料 : 使用不同尺寸的杯子裝飲料。不同尺寸有不同售價。
  • 炸薯條 : 薯條有不同尺寸份量。不同尺寸有不同售價。

進一步對飲料尺寸、薯條尺寸,與漢堡組成的方式做不同定義

飲料尺寸

利用枚舉 (enumeration) 定義飲料尺寸與售價,並建立飲料尺寸的資料結構來儲存尺寸和售價的資訊。

飲料尺寸售價
特大(venti)10.0
大(grande)7.5
中(tall)5.0
小大(short)2.5

薯條尺寸

利用枚舉 (enumeration) 定義薯條尺寸與售價,並建立薯條尺寸的資料結構來儲存尺寸和售價的資訊。

薯條尺寸售價
特大(supersize)10.0
大(large)7.5
中(medium)5.0

漢堡組成

將漢堡組成定義成一種產品,指定漢堡建構子需要傳入肉類(meat)肉片數量(piece) 以產生不同類型的漢堡。

製作者 (Creator)

透過枚舉建立尺寸資訊的資料結構,當製作者需要生產不同尺寸的飲料/薯條時,只需要將資料結構傳遞給飲料/薯條的建構子,便能簡化產生具體尺寸的飲料/薯條的產品。

另外,將漢堡組成定義成產品的好處,在於製作者要生產漢堡時,也只需要傳遞對應的肉類和肉片數量,便可以生產不同具體類型的漢堡,如:牛肉漢堡、豬肉漢堡或魚肉漢堡。

為了滿足套餐組合的需求,分別需要定義漢堡製作者(BurgerCreator)飲料製作者(DrinkCreator)薯條製作者(FriedCreator)

同時,為了讓套餐組合時可直接選用,還需要分別建立不同的 漢堡/飲料和尺寸/薯條尺寸 的製作者,並讓其繼承該項產品類型的製作者。

製作者類型製作者名稱建構子需求
漢堡製作者牛肉漢堡製作者肉類: 牛肉, 肉片量: 1 片 (預設)
漢堡製作者豬肉漢堡製作者肉類: 豬肉, 肉片量: 1 片 (預設)
漢堡製作者魚肉漢堡製作者肉類: 魚肉, 肉片量: 1 片 (預設)
飲料製作者可樂製作者飲料尺寸: [ 特大、大、中、小 ]
飲料製作者蘇打製作者飲料尺寸: [ 特大、大、中、小 ]
飲料製作者綠茶製作者飲料尺寸: [ 特大、大、中、小 ]
薯條製作者薯條製作者薯條尺寸: [ 特大、大、中 ]

套餐組合 (combo)

套餐組合直接從已定義好的各類產品製作者進行挑選,以組成漢堡、飲料和薯條的套餐內容;透過選擇不同的產品製作者,便能夠快速地建立起不同的套餐內容

套餐名稱套餐內容
牛肉漢堡套餐[牛肉漢堡、特大杯可樂、特大份薯條]
豬肉漢堡套餐[豬肉漢堡、大杯可樂、中份薯條]
魚肉漢堡套餐[魚肉漢堡、大杯綠茶、中份薯條]

Demo

用 Python 撰寫 Abstract Factory Pattern 的示例。
Source code

示例 : 顯示套餐內容

不同的具體套餐組合都繼承了套餐(Combo) 類別,若想顯示不同具體的套餐的內容時,便可調用同一組程式碼來達成目的

order information

# 傳入 Combo 類,即具體的套餐
def order_information(combo: Combo):
    # 獲取套餐內的漢堡
    burger = combo.get_burger()

    # 獲取套餐內的飲料
    drink = combo.get_drink()

    # 獲取套餐內的薯條
    fries = combo.get_fries()

    # 整合套餐內容的產品資訊,並計算整份套餐的售價
    description = {
        'burger': burger.get_description(),
        'drink': drink.get_description(),
        'fries': fries.get_description(),
        'total_price': burger.price + drink.price + fries.price
    }

    # 打印套餐類型的售價
    print(f'Combo: {combo.__class__.__name__}')
    print(description)
    print('-' * 30)

主程式

if __name__ == '__main__':
    # 生產豬肉漢堡套餐
    pork_combo = PorkBurgerCombo()
    # 打印套餐內容
    order_information(pork_combo)

    # 生產魚肉漢堡套餐
    fish_combo = FishBurgerCombo() 
    # 打印套餐內容
    order_information(fish_combo)

    # 生產牛肉漢堡套餐
    hamburger_combo = SupersizeHamburgerCombo()
    # 打印套餐內容
    order_information(hamburger_combo)

執行結果

小結

在 Factory Method Pattern 和本篇 Abstract Factory Pattern 中,我沒有使用工廠這一翻譯措辭的原因:

我認為工廠的定義是可變的,依據不同的場景會對工廠一詞有不同的定義;
但工廠的目的是不變的,即生產產品,並且不需要關注由誰生產產品。

因此,我才使用了 產品、製作者、製作者群組 等詞來表達我認知的 Factory Method Pattern 和 Abstract Factory Pattern。

在本文的示例中所採用的編成架構,將各項具體單品(產品)、具體生產者,以及套餐組合進行分層,並且讓每一層只專注自己的變動,如 :

  • 產品層只需要關注單品的參數,如名稱、售價,且單品參數的改變並不影響生產者層和套餐組合層的處理方式,以此為產品層異動保留了極大的彈性

  • 生產者層同樣只考慮該如何生產出具體的產品,圍繞著建構子傳遞的肉類或尺寸的既有資訊進行拓展,而不是去改變肉類或尺寸的資訊

  • 套餐組合層也僅考慮選用哪些具體生產者,來定義套餐組合的內容

未來無論是要增加新的套餐組合或是新的產品,不會(或非常小)對於現有的程式碼與軟體架構造成改動。

我覺得這是一個,很好的闡述了鬆散耦合軟體框架的範例