Design Pattern (7) - Abstract Factory Pattern (抽象工廠模式)

您可於此 design_pattern repo 下載 Design Pattern 系列程式碼。

引言:全球化的挑戰擴展

想像一下,隨著你的飲料點餐系統在全球範圍內的擴展,你面臨著如何滿足不同地區顧客特定偏好的挑戰。

需求:滿足全球化的味蕾

隨著業務的全球化擴展,不同地區的顧客有著不同的偏好。且我們也不能只賣紅茶及綠茶,需要為我們的菜單增加新的飲品,一邊新增菜單一邊擴展店舖。

物件導向分析(OOA)

  • public func createBeverage(beverageName: String) -> Beverage? {
        var beverage: Beverage?
    
        switch beverageName {
        case "black tea":
            beverage = EarlGreyBlackTea()
        case "green tea":
            beverage = SenchaGreenTea()
        default:
            break
        }
    
        return beverage
    }
    
  • override fun createBeverage(beverageName: String): Beverage? {
        return when (beverageName) {
            "black tea" -> CeylonBlackTea()
            "green tea" -> GyokuroGreenTea()
            else -> null
        }
    }
    

如何處理多個產品在不同分店的組合,這時就需要用到 Abstract Factory Pattern

察覺 Forces

當我們每增加一種飲品到菜單中,我們必須要修改所有的 Factory 中的方法,違反了 Open Closed Principle

套用 Solution

看清楚整個 Context,察覺 Forces 後,就可以套用 Abstract Factory Pattern 來解決這個問題

先來看一下 Abstract Factory Pattern 的 UML

透過將工廠抽象,使子類別能創建一系列的實體物件。

抽象工廠有個重要的判斷方式,當你所要創建的產品是一整個系列產品,且不同需求要創建不同系列,這個關係能夠畫成二維關係,這時就非常適合使用抽象工廠來建立產品

如下圖

Country / Tea BlackTea GreenTea MilkTea
US Flavor Ceylon(錫蘭) Gyokuro(玉露) Thai (泰奶)
EU Flavor EarlGrey(伯爵) Sencha(煎茶) Masala Chai (印度馬薩拉))
JP Flavor Assam(阿薩姆) Matcha(抹茶) Hokkaido(北海道奶茶)

讓我們根據上面的茶家族修改一下 UML 及程式碼吧(這邊只是要表達二維關係的概念,僅先實作紅茶及綠茶的部分)

如此我們就得到了一個全新的 Resulting Context

物件導向程式設計 (OOP)

再來我們就可以開始進行物件導向程式開發

  • public protocol BlackTea {
    }
    
    public class CeylonBlackTea: BlackTea {
    
    }
    
    public class EarlGreyBlackTea: BlackTea {
    
    }
    
    public protocol GreenTea {
    }
    
    public class GyokuroGreenTea: GreenTea {
    
    }
    
    public class SenchaGreenTea: GreenTea {
    
    }
    
    public protocol BeverageFactory {
        func createBlackTea() -> BlackTea?
        func createGreenTea() -> GreenTea?
    }
    
    open class USBeverageFactory: BeverageFactory {
    
        public init() {}
    
        public func createBlackTea() -> BlackTea? {
            return CeylonBlackTea()
        }
    
        public func createGreenTea() -> GreenTea? {
            return GyokuroGreenTea()
        }
    }
    
    open class EUBeverageFactory: BeverageFactory {
    
        public init() {}
    
        public func createBlackTea() -> BlackTea? {
            return EarlGreyBlackTea()
        }
    
        public func createGreenTea() -> GreenTea? {
            return SenchaGreenTea()
        }
    }
    
    let usBeverageFactory = USBeverageFactory()
    let usBlackTea = usBeverageFactory.createBlackTea()
    let usGreenTea = usBeverageFactory.createGreenTea()
    
    print("usBlackTea is \(usBlackTea)")
    print("usGreenTea is \(usGreenTea)")
    
    let euBeverageFactory = EUBeverageFactory()
    let euBlackTea = euBeverageFactory.createBlackTea()
    let euGreenTea = euBeverageFactory.createGreenTea()
    
    print("euBlackTea is \(euBlackTea)")
    print("euGreenTea is \(euGreenTea)")
    
  • interface BlackTea {
    }
    
    class CeylonBlackTea: BlackTea {
    }
    
    class EarlGreyBlackTea: BlackTea {
    }
    
    interface GreenTea {
    }
    
    class GyokuroGreenTea: GreenTea {
    }
    
    class SenchaGreenTea: GreenTea {
    }
    
    interface BeverageFactory {
        fun createBlackTea(): BlackTea
        fun createGreenTea(): GreenTea
    }
    
    class USBeverageFactory: BeverageFactory {
    
        override fun createBlackTea(): BlackTea {
            return CeylonBlackTea()
        }
    
        override fun createGreenTea(): GreenTea {
            return GyokuroGreenTea()
        }
    }
    
    class EUBeverageFactory: BeverageFactory {
    
        override fun createBlackTea(): BlackTea {
            return EarlGreyBlackTea()
        }
    
        override fun createGreenTea(): GreenTea {
            return SenchaGreenTea()
        }
    }
    
    val usBeverageFactory = USBeverageFactory()
    val usBlackTea = usBeverageFactory.createBlackTea()
    val usGreenTea = usBeverageFactory.createGreenTea()
    
    print("usBlackTea is $usBlackTea")
    print("usGreenTea is $usGreenTea")
    
    val euBeverageFactory = EUBeverageFactory()
    val euBlackTea = euBeverageFactory.createBlackTea()
    val euGreenTea = euBeverageFactory.createGreenTea()
    
    print("euBlackTea is $euBlackTea")
    print("euGreenTea is $euGreenTea")
    

使用抽象工廠後,分店不需要知道實際是什麼茶,只要知道跟自己地區的飲料工廠取得 紅/綠/奶茶,這邊也運用到了 Dependency Inversion Principle,工廠及產品兩者皆依賴於抽象。

補充說明

下面舉幾種二維關係可以使用 Abstract Factory Pattern 的例子


做跨平台應用時,會遇到不同平台與各種 UI 元件的組合

OS / UI Components Button Checkbox
Linux LinuxButton LinuxCheckbox
MacOS MacButton MacCheckbox
Windows WinButton WinCheckbox

做 App 時,會遇到需要支持 Light/Dark Mode 與各種 UI 元件的組合

Theme / UI Components Button Checkbox
Light Mode LightModeButton LightModeCheckbox
Dark Mode DarkModeButton DarkModeCheckbox

做 IoT 系統時,會遇到 ZWave/Zigbeee 傳輸協議與各種 Iot 裝置的組合

Protocol / Device Dimmer Hue Thermostat
ZWave ZWDimmer ZWHue ZWThermostat
Zigbee ZBDimmer ZBHue ZBThermostat

Factory Method Pattern vs Abstract Factory Pattern

Factory Method Pattern 工廠方法模式

對每一種產品提供相應的工廠去建立產品,產品擴充性高。

Abstract Factory Pattern 抽象工廠模式

對一整個系列的產品進行抽象建立,工廠擴充性高,如加入新的系列產品,但產品擴充性低,所有的工廠都必須加入新產品。

總結

在本文中,我們探討了工廠方法模式和抽象工廠模式的區別。工廠方法模式專注於單一產品的建立,提供高產品擴充性;而抽象工廠模式則針對一系列產品提供建立機制,提供工廠的高擴充性但產品擴充性較低。

我們來看看工廠方法用到哪些 Design Principle

  • Encapsulate What Varies
  • Loose Coupling
  • Program to Interfaces
  • Single Responsibility Principle
  • Open Closed Principle
  • Dependency Inversion Principle

參考

Note: 如果有任何建議、問題或不同想法,歡迎留言或寄信給我,可以一起討論進步成長🙂




    Enjoy Reading This Article?

    Here are some more articles you might like to read next:

  • 💡 一台電腦操作多個 GitHub 帳號:最簡單快速的 SSH 設定方法
  • 🚀 如何使用 Excalidraw AI 快速生成專業級圖表,提升工作效率!
  • Setup Development Environment on a New macOS
  • Design Pattern (28) - Interpreter Pattern (解譯器模式)
  • Design Pattern (27) - Visitor Pattern (訪問者模式)