Design Pattern Factory

工廠模式

封裝建立物件的邏輯,讓使用者無須在意物件創建的相關邏輯(e.g. 讀取設定檔…)。

在工廠模式中,會將封裝建立物件邏輯的類別稱為工廠,而實際建立的物件稱為產品,就像普通人並不會知道工廠運作的邏輯,但知道工廠生產什麼產品一樣。 工廠模式的目的是讓使用者(需要物件的function或邏輯),無需在意這些物件建立的相關邏輯。

以下會以漫畫(comic)作為產品 來舉例說明,假設我有兩種漫畫,分別是 funny 跟 adventure。 我想要取得漫畫物件最簡單的方法是透過 if 來判斷該創立哪種物件。

from abc import ABC, abstractmethod
class Book(ABC):
    @abstractmethod
    def content(self) -> str:
        pass
    @abstractmethod
    def cost(self) -> int:
        pass

class FunnyComics(Book):
    def content(self) -> str:
        return 'This is funny comic'
    def cost(self) -> int:
        return 4
class AdventureComics(Book):
    def content(self) -> str:
        return 'This is adventure comic'
    def cost(self) -> int:
        return 3

def buy_book(book_type:str) -> Book:
    if book_type == "Funny Comic":
        book = FunnyComics()
    elif book_type == "Adventure Comic":
        book = AdventureComics()
    print(book.content())
    print(book.cost())

if __name__ == '__main__':
    buy_book("Funny Comic")
    buy_book("Adventure Comic")

但是會發現當漫畫的種類越來越多的時候,buy_book 內的邏輯會越來越多,並且會一直需要改動 buy_book 這個 function

簡單工廠

所有的產品物件類別都由一個共同的工廠類別或介面來管理。

其實是將 if 等相關判斷式另外包進去一個類別做管理

class SimpleBookFactory():
    def create_book(self,book_type:str) -> Book:
        if book_type == "Funny Comic":
            book = FunnyComics()
        elif book_type == "Adventure Comic":
            book = AdventureComics()
        return book

def buy_book(book_type:str):
    book_factory = SimpleBookFactory()
    book = book_factory.create_book(book_type)
    print(book.content())
    print(book.cost())

這樣做的好處在於對於 buy_book 來說,不用在意 FunnyComicsAdventureComics 這兩個類別的實作細節。

只需透過傳遞參數給 SimpleBookFactory 即可。

但是這樣做依然有當新增類別時需要修改到 SimpleBookFactory 這個問題。

from abc import ABC, abstractmethod
class Book(ABC):
    @abstractmethod
    def content(self) -> str:
        pass
    @abstractmethod
    def cost(self) -> int:
        pass

class FunnyComics(Book):
    def content(self) -> str:
        return 'This is funny comic'
    def cost(self) -> int:
        return 4
class AdventureComics(Book):
    def content(self) -> str:
        return 'This is adventure comic'
    def cost(self) -> int:
        return 3

class SimpleBookFactory():
    def create_book(self,book_type:str)-> Book:
        if book_type == "Funny Comic":
            book = FunnyComics()
        elif book_type == "Adventure Comic":
            book = AdventureComics()
        return book
    
def buy_book(book_type:str):
    book_factory = SimpleBookFactory()
    book = book_factory.create_book(book_type)
    print(book.content())
    print(book.cost())

if __name__ == '__main__':
    buy_book('Funny Comic')
    buy_book('Adventure Comic')

工廠方法

設計工廠介面,產生產品物件由根據工廠介面建立的子類別來管理。

首先先來將簡單工廠的設計轉為工廠模式的設計。

先定義一個工廠類別的介面,並且都應該返回一個產品類別的物件。

class BookFactory(ABC):
    @abstractmethod
    def create_book(self) -> Book:
        pass

根據這個介面,來修改原本的 buy_book,對於 buy_book 來說,只需知道傳遞進來的參數是 BookFactory 類別即可。

所以除非是更改到了 BookFactory 這個介面,否則不會因為新增了工廠或是產品類別而需要更改到 buy_book

def buy_book(factory:BookFactory):
    book_factory = factory()
    book = book_factory.create_book()
    print(book.content())
    print(book.cost())

接著設計工廠類別

class FunnyBookFactory(BookFactory):
    def create_book(self)-> Book:
        return FunnyComics()

class AdventureBookFactory(BookFactory):
    def create_book(self)-> Book:
        return AdventureComics()

相對於簡單工場類別,假設我今天需要新增一個新的 Book 類型,我只須新增一個工廠類別與產品類別就可以。

不必擔心新增了 Book 類型需要去改動到原本的工廠類別,不像簡單工場新增一個產品就需要更改到 SimpleBookFactory 類別。

buy_book 一樣不用在意如何去建立產品物件,因為這些都由各自對應的工廠負責。

e.g. 假設要新增一個 love comic,只需新增如下

class LoveComics(Book):
    def content(self) -> str:
        return 'This is love comic'
    def cost(self) -> int:
        return 6

class LoveComicsFactory(BookFactory):
    def create_book(self)-> Book:
        return LoveComics()

不會更改到 buy_book,甚至先前建立的 FunnyComicsFactory 或是 AdventureComicsFactory 類別。

from abc import ABC, abstractmethod
class Book(ABC):
    @abstractmethod
    def content(self) -> str:
        pass
    @abstractmethod
    def cost(self) -> int:
        pass

class FunnyComics(Book):
    def content(self) -> str:
        return 'This is funny comic'
    def cost(self) -> int:
        return 4
    
class AdventureComics(Book):
    def content(self) -> str:
        return 'This is adventure comic'
    def cost(self) -> int:
        return 3
    
class LoveComics(Book):
    def content(self) -> str:
        return 'This is love comic'
    def cost(self) -> int:
        return 6
    
class BookFactory(ABC):
    @abstractmethod
    def create_book(self,book_type:str) -> Book:
        pass

class FunnyBookFactory(BookFactory):
    def create_book(self)-> Book:
        return FunnyComics()

class AdventureBookFactory(BookFactory):
    def create_book(self)-> Book:
        return AdventureComics()
    
class LoveBookFactory(BookFactory):
    def create_book(self)-> Book:
        return LoveComics()

def buy_book(factory:BookFactory):
    book_factory = factory()
    book = book_factory.create_book()
    print(book.content())
    print(book.cost())

if __name__ == '__main__':
    buy_book(FunnyBookFactory)

    buy_book(AdventureBookFactory)

    buy_book(LoveBookFactory)

抽象工廠

設計工廠介面用於建立一系列相關的產品。

假設當類別變多了,例如今天每種書又都分為電子書跟實體書,前面使用的工廠方法所需設計的類別就可能會變很多。

這時就可以使用抽象工廠來解決這個問題。

先設計抽象類別,每個工廠會分別建立實體書跟電子書。

class BookFactory(ABC):
    @abstractmethod
    def create_book()-> Book:
        pass
    @abstractmethod
    def create_e_book()-> Book:
        pass

擴充電子書產品類別(Book)

class FunnyComicsEBook(Book):
    def content(self) -> str:
        return 'This is funny comic e-book'
    def cost(self) -> int:
        return 3
class AdventureComicsEBook(Book):
    def content(self) -> str:
        return 'This is adventure comic e-book'
    def cost(self) -> int:
        return 2

根據 BookFactory 設計子類別

class FunnyBookFactory(BookFactory):
    def create_book(self)-> Book:
        return FunnyComics()
    def create_e_book(self)-> Book:
        return FunnyComicsEBook()

class AdventureBookFactory(BookFactory):
    def create_book(self)-> Book:
        return AdventureComics()
    def create_e_book(self)-> Book:
        return AdventureComicsEBook()
from abc import ABC, abstractmethod

class Book(ABC):
    @abstractmethod
    def content(self) -> str:
        pass
    @abstractmethod
    def cost(self) -> int:
        pass

class FunnyComics(Book):
    def content(self) -> str:
        return 'This is funny comic'
    def cost(self) -> int:
        return 4
    
class FunnyComicsEBook(Book):
    def content(self) -> str:
        return 'This is funny comic e-book'
    def cost(self) -> int:
        return 3
    
class AdventureComics(Book):
    def content(self) -> str:
        return 'This is adventure comic'
    def cost(self) -> int:
        return 3

class AdventureComicsEBook(Book):
    def content(self) -> str:
        return 'This is adventure comic e-book'
    def cost(self) -> int:
        return 2

class BookFactory(ABC):
    @abstractmethod
    def create_book() -> Book:
        pass
    @abstractmethod
    def create_e_book()-> Book:
        pass

class FunnyBookFactory(BookFactory):
    def create_book(self)-> Book:
        return FunnyComics()
    def create_e_book(self)-> Book:
        return FunnyComicsEBook()

class AdventureBookFactory(BookFactory):
    def create_book(self)-> Book:
        return AdventureComics()
    def create_e_book(self)-> Book:
        return AdventureComicsEBook()

def buy_book(factory:BookFactory):
    book_factory = factory()
    book = book_factory.create_book()
    print(book.content())
    print(book.cost())
    e_book = book_factory.create_e_book()
    print(e_book.content())
    print(e_book.cost())

if __name__ == '__main__':
    buy_book(FunnyBookFactory)

    buy_book(AdventureBookFactory)

相對於工廠方法,抽象工廠其實更注重於產出彼此相關的產品。 工廠方法則是專門產生一個產品的工廠。

雖然不同的抽象工廠可能生產的產品具備相同功能,但它們的具體實作會根據工廠有所不同。

例如本篇例子FunnyBookFactoryAdventureBookFactory 底下的產品都分成實體書跟電子書。 但是 content 會根據是 FunnyBookFactory 還是 AdventureBookFactory 而不同。

參考:

使用 Hugo 建立
主題 StackJimmy 設計