iT邦幫忙

2021 iThome 鐵人賽

2
Software Development

淺談中台架構、DDD與Python實踐系列 第 8

【Day 08】工廠方法設計模式(Python)

前言

上一篇我們討論DDD的戰術設計,它建議引用各種設計模式,提高生產力,因此接下來,就來介紹各種設計模式(Design Patterns),我們會使用Python開發程式,由於Python的自由開放,可以使各種設計模式變的更強大,可以藉由下列程式碼得到印證。

我們先從最簡單的工廠方法(Factory Method)開始討論。

工廠方法(Factory Method)

『工廠方法』顧名思義,這個設計模式是一個生產物件的工廠,使用者不須知道物件是如何生產的,只要指名要生產的物件名稱,工廠就會幫使用者生產所需的物件。例如,一個工廠有生產轎車(Car)、公車(Bus)、卡車(Truck)等車子,這些車種各自定義為一個類別如下:

class Car:
    def __init__(self, maker='', num_of_doors=4,  fuel_type='oil'):
        self.maker, self.num_of_doors,  self.fuel_type = maker, num_of_doors,  fuel_type

    def __repr__(self):
        return 'I\'m a car.'

class Bus:
    def __init__(self, maker='', size=''):
        self.maker, self.size = maker, size

    def __repr__(self):
        return 'I\'m a bus.'

class Truck:
    def __init__(self, maker='', weight=10):
        self.maker, self.weight = maker, weight

    def __repr__(self):
        return 'I\'m a truck.'

每個類別的屬性都不同,也不須繼承同一父類別。

接著定義一個很簡單的工廠方法,其中A/B/C各對應一種車子:

def FactoryInterface(classname):
    # 物件名稱與類別對照表
    name_class_mapping = {'A':Car, 'B':Bus, 'C':Truck}
    
    # 建立物件
    return name_class_mapping[classname]()

關鍵程式碼 dict1classname,直接產生一個物件。

測試看看:

# 測試
obj1 = FactoryInterface('A')
print(obj1)

obj2 = FactoryInterface('B')
print(obj2)

obj3 = FactoryInterface('C')
print(obj3)

輸出如下:
I'm a car.
I'm a bus.
I'm a truck.

** 使用 eval(),甚至不需要 FactoryInterface 方法**,雖然方便,但不易偵錯,只有在執行時,才能發覺錯誤。

obj_name = 'Car'
obj = eval(obj_name + '()')
print(obj)

產生有不同屬性的物件

上述車種類別各有不同的屬性,我們如何在建立物件時,指定各自所需的屬性,解決方案如下:

  1. 修改工廠方法:加一參數 arg,接收一個字典。
# 工廠方法
def FactoryInterface_2(classname, arg):
    # 物件名稱與類別對照表
    name_class_mapping = {'A':Car, 'B':Bus, 'C':Truck}
    
    # 建立物件
    return name_class_mapping[classname](**arg)
  1. 指定物件時,夾帶一個字典,指定物件各個參數及對應的值:
# 測試
arg = {'maker':'BMW', 'num_of_doors':4,  'fuel_type':'oil'}
obj1 = FactoryInterface_2('A', arg)
print(obj1)
print(obj1.maker)
print()

arg = {'maker':'Volvo', 'size':'large'}
obj2 = FactoryInterface_2('B', arg)
print(obj2)
print(obj2.size)
print()

輸出如下,執行無誤:
I'm a car.
BMW

I'm a bus.
large

接受多餘的參數

有時候使用者不知道物件的詳細規格,可能會多傳送一些參數,如何避免錯誤產生,很簡單,將每個物件加一個**ignore 即可,多餘的參數會被它全部接收。

class Car:
    def __init__(self, maker='', num_of_doors=4,  fuel_type='oil', **ignore):
        self.maker, self.num_of_doors,  self.fuel_type = maker, num_of_doors,  fuel_type

    def __repr__(self):
        return 'I\'m a car.'

class Bus:
    def __init__(self, maker='', size='', **ignore):
        self.maker, self.size = maker, size

    def __repr__(self):
        return 'I\'m a bus.'

class Truck:
    def __init__(self, maker='', weight=10, **ignore):
        self.maker, self.weight = maker, weight

    def __repr__(self):
        return 'I\'m a truck.'

測試一下:

# 多 size 參數
arg = {'maker':'BMW', 'num_of_doors':4,  'fuel_type':'oil', 'size':'large'}
obj1 = FactoryInterface_2('A', arg)
print(obj1)
print(obj1.maker)
print()

# 多 num_of_doors 參數
arg = {'maker':'Volvo', 'num_of_doors':4, 'size':'large'}
obj2 = FactoryInterface_2('B', arg)
print(obj2)
print(obj2.size)
print()

輸出如下,執行無誤:
I'm a car.
BMW

I'm a bus.
large

如果傳送時,缺少一些參數也不會有問題,因為每個參數都可以指定預設值,這都拜Python強大的威力。

應用時機

『工廠方法』可以用在甚麼場合呢? 以筆者開發經驗,常發生在資料交換時,譬如 LINE BOT 訊息格式有很多種:

  • Text message
  • Sticker message
  • Image message
  • Video message
  • Audio message
  • Location message
  • Imagemap message
  • Template message
  • Flex Message

每一種訊息內容包含的欄位各不相同,通常會使用類別來描述每一種訊息格式,當 LINE BOT 在接收到請求時,要依據表頭(Header),決定要創建何種訊息回應,這正好可以使用『工廠方法』來實踐此一情境。
https://ithelp.ithome.com.tw/upload/images/20211018/20001976wXtRDAjlKJ.png

結語

從以上介紹,可以知道『工廠方法』可以用在有很多訊息種類或類別的情境中,不需要寫很多的If ... elif... else,去判斷要創建何種物件,透過『工廠方法』可以免除判斷式的維護,同時,利用 Python 語言強大的威力,可以適用不同參數的物件,使設計模式更有彈性。

除了上面的工廠方法外,還有以下的延伸,我們後續再一一討論,Happy coding。

  1. 抽象工廠(Abstract Factory)
  2. 物件創建者(Builder)

市面上很多設計模式的書籍都只介紹各種模式的架構與實踐的程式碼,缺乏實際應用的場景與應用範例,如果不是開發過很多應用系統的資深工程師,其實是很難想像如何在開發專案應用設計模式,因此,也盼望各位讀者大德可以一起分享自身應用的經驗,惟有如此,設計模式才有可能被廣泛研究與使用。


上一篇
【Day 07】領域驅動設計的戰術設計(Tactical Design)
下一篇
【Day 09】配接器 設計模式(Python)
系列文
淺談中台架構、DDD與Python實踐10

尚未有邦友留言

立即登入留言