iT邦幫忙

第 11 屆 iT 邦幫忙鐵人賽

DAY 15
0

[Design Pattern] Abstract Factory 抽象工廠模式

桌子。

說起桌子,你會想到什麼呢?是 IKEA 的現代家具、維多利亞式的古典象徵,還是活像裝置藝術但不太確定哪邊是正面的東西?

我們也許能定義桌子:有桌面、有支撐本體的支架、有接觸地板的底部。

但風格就很難了,我頂多描述一下古典家具看起來...很古典、不太現代、好像很貴、我家沒有。但實在沒法用文字詳細定義他們。

如果今天有做家具工廠,要做出現代椅子、古典椅子、現代桌子、古典桌子,那我就需要四座工廠分頭生產囉?

不如換個方向想:桌子是一種家具,可以分現代、古典;椅子也是一種家具,也可以分現代、古典。那我們就可以開一個家具總工廠,其下有現代家具工廠會生產現代桌子、現代椅子,以及古典家具工廠會生產古典桌子、古典椅子。

今天如果一個住在古典豪宅的客戶來向家具總工廠訂購桌子、椅子,家具總工廠就會請古典家具工廠生產古典桌子、古典椅子給這位客戶。

14_abstract_factory_1
圖片引用自[2]

這次的主題,Abstract Factory 抽象工廠模式就是一種類似的創建型設計模式:透過它我們可以創建一系列相關的 product(比如說古典系列家具),而無需為每一種 product 定義具體的 class(不需要古典桌子開一家工廠,古典椅子又開另一家工廠)。

對照上面的例子,家具總工廠就是抽象工廠,而其下的現代家具工廠是一個具體工廠、古典家具工廠是另一個具體工廠。其中,想像桌子是一種抽象產品,而現代桌子、古典桌子分別是具體產品;椅子是一種抽象產品,而現代椅子、古典椅子分別是具體產品。

其中桌子、椅子這種家具類別一但定義出來了就很少新增,想像一下家具種類大概就是 IKEA 會看到的那些,不會下個月突然冒出一種專門用來放腳的家具(好詭異)。但是風格卻可以很多變,每次去 IKEA 大概都會看到不同風格的新家具(作者上次外出取材的時候在 IKEA 看到一張紫金搭配的椅子,這還是第一次看到這種高調風格出現在球衣以外的地方)。

為什麼需要 Abstract Factory?

擁有某特定環境變數的客戶只要調用抽象工廠就能獲得適合該環境的所有product。

比如說,上面提到一位住在古典豪宅的客戶,他在一開始會先設定環境變數:古典。之後他只要跟家具總工廠訂購桌子、椅子,他就能獲得風格一致的桌子、椅子。家具總工廠會看他的環境變數:古典,來決定調用具體的古典家具工廠來生產古典椅子、古典桌子給客戶。

除了居家風格以外,一個環境變數的真實例子則是作業系統:比如說我有一台電腦,環境變數就是電腦的作業系統,早在一開始就決定好是 MacOS。如果我要買滑鼠、鍵盤,我只要去電腦商城說我想要滑鼠、鍵盤,商城店員看我使用 MacOS,就會幫我找來 MacOS 專用的滑鼠、鍵盤。搞定。

此外,這種設計也滿足計算機科學中的開閉原則,使得程式碼更有彈性、更易維護。以及單一功能原則:具體工廠不必操心一組家具除了椅子以外是棋子還是塞子。家具工廠可以創建椅子、桌子,這都被事先獨立於抽象工廠裡面了,這使得程式碼更容易維護。

而壞處呢?[1]主張額外創造的 class 把程式碼變複雜了。不過顯然 Z>B,在大多情境中我們還是可以放心使用抽象工廠方法。

##xs 來張圖吧

14_abstract_factory_2

寫成程式碼吧

from abc import ABC

# 抽象的家具工廠
class FurnitureFactory(ABC):

    # 會做桌子
    def create_table(self):
        pass

    # 會做椅子
    def create_chair(self):
        pass


# 具體的古典家具工廠
class ClassicFurnitureFactory(FurnitureFactory):

    # 會做桌子
    def create_table(self):
        return ClassicTable()

    # 會做椅子
    def create_chair(self):
        return ClassicChair()


# 具體的現代家具工廠
class ModernFurnitureFactory(FurnitureFactory):
    def create_table(self):
        return ModernTable()

    def create_chair(self):
        return ModernChair()


# 抽象的椅子
class Chair(ABC):

    # 椅子可以坐
    def sit_on(self):
        pass


# 具體的古典椅子
class ClassicChair(Chair):

    # 椅子可以坐
    def sit_on(self):
        return "Client sits on a classic chair"


# 具體的現代椅子
class ModernChair(Chair):
    
    # 椅子可以坐
    def sit_on(self):
        return "Client sits on a modern chair"


# 抽象的桌子
class Table(ABC):

    # 桌子可以放東西
    def put_stuff_on(self):
        pass

    # 桌子跟椅子一起可以展示主人的品味
    def showcase_style(self, collaborator: Chair):
        pass


class ClassicTable(Table):
    def put_stuff_on(self):
        return "Client puts stuff on a classic table"

    # 古典桌子可以跟任一椅子一起展示主人的品味,像是古典桌子+古典椅子看起來風格一致,像是古典桌子+現代椅子可能看起來怪怪的
    def showcase_style(self, collaborator: Chair):
        return "{}, and then {}".format(self.put_stuff_on(), collaborator.sit_on())


class ModernTable(Table):
    def put_stuff_on(self):
        return "Client puts stuff on a modern table"

    # 現代桌子可以跟任一椅子一起展示主人的品味,像是現代桌子+現代椅子看起來風格一致,像是現代桌子+古典椅子可能看起來怪怪的
    def showcase_style(self, collaborator: Chair):
        return "{}, and then {}".format(self.put_stuff_on(), collaborator.sit_on())


# 客戶端跟家具總公司說他想要桌子、椅子
def client_code(factory: FurnitureFactory):

    # 客戶拿到的家具的風格取決於產自哪個具體工廠
    table = factory.create_table()
    chair = factory.create_chair()

    print("Let's try new table:", table.put_stuff_on())
    print("Let's try new chair:", chair.sit_on())
    print("How do they look when client use them together?", table.showcase_style(chair))


if __name__ == "__main__":

    # 家具總公司根據客戶的古典風格,生產古典風格的桌子、椅子給客戶試試看
    print("Client is trying classic style furniture")
    client_code(ClassicFurnitureFactory())

    print("\n")

    # 家具總公司根據客戶的現代風格,生產現代風格的桌子、椅子給客戶試試看
    print("Client is trying modern style furniture")
    client_code(ModernFurnitureFactory())

加油!

這次文章連載至此已經快一半了,我們再接再厲,繼續把後半段的design pattern學完吧!

Reference

  1. Dive Into Design Patterns by Alexander Shvets
  2. https://refactoring.guru
  3. https://sourcemaking.com

作者:Allen


上一篇
[Design Pattern] Flyweight 輕量模式
下一篇
[Design Pattern] State 狀態模式
系列文
什麼?又是/不只是 Design Patterns!?32

尚未有邦友留言

立即登入留言