Design Patterns @ Python -- Factories 工廠模式筆記

Design Patterns @Python Course Note

  • Factories 工廠模式

    • 概述:

      • 工廠模式(Factory Pattern)是一種創建型設計模式,它允許客戶端在不指定具體類型的情況下創建對象。
    • 用途及時機:

      • 當物件的建構需要支援多種方式 → 需要根據不同的參數創建不同類型實例的情況。
      • 需要創建的對象具有複雜的構造方法,而且需要進行重複使用。工廠模式可以將創建對象的邏輯封裝在工廠類中,客戶端只需要調用工廠類的方法即可獲得所需的對象。
        • 封裝: 不讓(遮蔽)使用者接觸到不支援的方法 → 區別工廠來創建物件
          • 例如有三合一的印表機原型,定義了列印、掃描、傳真三種方法,但如果某一台只支援列印的傳統印表機物件,繼承了該原型物件,但即便已經不實作掃描及傳真方法,仍會讓 User 看到這個方法,造成誤用
        • 需要創建的對象存在多種類型,根據不同的條件進行創建。
      • 工廠模式不適用於以下類型:
        • 只需要創建一個對象的情況。在這種情況下,可以直接創建對象,而不需要使用工廠模式。
        • 創建對象的過程很簡單,不需要進行封裝。在這種情況下,直接創建對象即可。
    • 工廠類型:

      • 簡單工廠模式 (Factory Method or Simple Factory)
        • 這個方式將創建對象的代碼與使用該對象的代碼分離,對每個輸入的參數返回不同對象的方法
        • 解決直接透過 init + if 的條件式創建 —> 違反OCP介面開閉原則(對擴展開放,對修改封閉)
      • 抽象工廠模式 (Abstract Factory)
        • 當創建目標對象為多種類型時,就需要抽象工廠類,讓一個工廠可以生產多種類型產品
    • 流程示意:


      (以上圖片摘自: Wikipedia - Abstract factory pattern)

  • 工廠模式範例說明:

    • 首先來看一個二維座標 Point 物件,要創建一個二維座標物件:
      • 可以直接使用 x, y 座標來表示
      • 也可以使用極座標法,使用向量長度 r 以及與水平夾角的角度 θ 來表示:
        • x = r * cos(θ); y = r * sin(θ)
from enum import Enum
from math import *

class CoordinateSystem(Enum):
    CARTESIAN = 1
    POLAR = 2

class Point:
    # 不合法的 __init__ 寫法 (只能有一個 __init__ 方法)
    #def __init__(self, x, y):
    #        self.x = x
    #        self.y = y       
            
    #def __init__(self, a, b):
    #        self.x = a * cos(b)
    #        self.y = a * sin(b)  

    # 違反 OCP Rule --> 已經修改到類別中
    def __init__(self, a, b, system=CoordinateSystem.CARTESIAN):
        if system == CoordinateSystem.CARTESIAN:
            self.x = a
            self.y = b
        elif system == CoordinateSystem.POLAR:
            self.x = a * cos(b)
            self.y = a * sin(b)
  • 簡單工廠模式 (Simple Factory):
from math import *
from functools import reduce

class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __str__(self):
        return f'x: {self.x}, y: {self.y}'        

    # 將工廠方法用在類別物件下的靜態方法 ---> 這仍然違反OCP介面開閉原則
    @staticmethod 
    def new_cartesian_point(x, y):
        return Point(x, y)

    @staticmethod
    def new_polar_point(rho, theta):
        angle = radians(theta)
        return Point(rho * cos(angle), rho * sin(angle))

# 將工廠方法獨立出來到一個工廠類別中,這樣就不需要動到目標物件,而透過工廠擴充方法來使用不同方法創建物件
class PointFactory:
    @staticmethod
    def new_cartesian_point(x, y):
        return Point(x, y)

    @staticmethod
    def new_polar_point(rho, theta):
        angle = radians(theta)
        return Point(rho * cos(angle), rho * sin(angle))

p1 = Point(1, 2)
p2 = Point.new_cartesian_point(3, 4)
p3 = Point.new_polar_point(6, 60)
p4 = PointFactory.new_cartesian_point(7, 8)
p5 = PointFactory.new_polar_point(6, 90)
result = reduce(lambda x, y:  str(x) + '\n' + str(y) , [p1, p2, p3, p4, p5])
print(result)
  • 抽象工廠模式 (Abstract Factory):
from abc import ABC # Abstract Base Class
from enum import Enum, auto

# 熱飲基類
class HotDrink(ABC):
    def consume(self):
        pass

# 茶 --> 繼承熱飲基類
class Tea(HotDrink):
    def consume(self):
        print('This tea is nice but I\'d prefer it with milk')

# 咖啡 --> 繼承熱飲基類
class Coffee(HotDrink):
    def consume(self):
        print('This coffee is delicious')

# 創建一個熱飲工廠基類
class HotDrinkFactory(ABC):
    def prepare(self, amount):
        pass

# 創建一個茶工廠類別,繼承熱飲工廠基類 --> 以創建茶物件
class TeaFactory(HotDrinkFactory):
    def prepare(self, amount):
        print(f'Put in tea bag, boil water, pour {amount}ml, enjoy!')
        return Tea()

# 創建一個咖啡工廠類別,繼承熱飲工廠基類 --> 以創建咖啡物件
class CoffeeFactory(HotDrinkFactory):
    def prepare(self, amount):
        print(f'Grind some beans, boil water, pour {amount}ml, enjoy!')
        return Coffee()

def make_drink(type):
    if type == 'tea':
        return TeaFactory().prepare(200)
    elif type == 'coffee':
        return CoffeeFactory().prepare(50)
    else:
        return None

make_drink('tea')
make_drink('coffee')
1個讚