一個入口類別要協調一堆子系統,怎麼不變成 God Object?

Medium★★★★Studied

FromDesign Patterns (GoF)物件導向設計面試大話設計模式

Concepts practiced🧱 外觀 (Facade)📏 單一職責原則 (SRP)📏 迪米特法則 (Law of Demeter)

Convention: read the requirements & constraints first, sketch your own design, then expand the reference to compare.

📚 From the Books

一個入口類別要協調一堆子系統,怎麼不變成 God Object?ProblemMedium設計情境

需求:設計一台販賣機(或任何「單一入口、背後一堆子系統」的裝置)。客戶端只想要 insertMoney()selectProduct() 這種簡單入口,但背後其實要協調庫存、金流、找零、交易紀錄。若把這些全塞進 VendingMachine,它就變成 God Object。怎麼設計?

🎯 Scenario

// 反例:VendingMachine 一手包辦庫存、金流、找零、紀錄 → 上帝物件
class VendingMachine {
    private val racks = mutableListOf<Rack>()
    fun buy(code: String, cash: Money) {
        val rack = racks.first { it.product.code == code }   // 庫存邏輯
        if (rack.count == 0) error("out of stock")
        val change = cash - rack.product.price               // 金流/找零邏輯
        if (change < 0) error("insufficient")
        rack.count--                                         // 分發+庫存
        ledger.add(Transaction(code, cash, change))          // 交易紀錄
        // …驗證、找零、紀錄全長在同一個類別裡,越長越肥
    }
}

需求關鍵字:要對外提供一個簡單統一的入口,同時背後有多個各自有職責的子系統。這是 Facade 的主場——但關鍵是 facade 要輕、只委派

🧠 Intuition

TIP

先別看下面。把「對外的入口」和「實際幹活的人」分開:VendingMachine 只當門面,把庫存交給 InventoryManager、金流交給 PaymentProcessor、把進行中的購買狀態收進 Transaction。門面本身不含業務細節。

⚖️ Reference Design

主解法 — Facade:輕量入口 + 委派給 SRP 元件
class InventoryManager(private val racks: List<Rack>) {
    fun isAvailable(code: String) = racks.any { it.product.code == code && it.count > 0 }
    fun dispense(code: String): Product { /* 遞減庫存、釋出商品 */ }
}
class PaymentProcessor {
    fun charge(price: Money, cash: Money): Money {           // 回傳找零
        require(cash >= price) { "insufficient" }
        return cash - price
    }
}

// Facade:只協調、只委派,自己不含庫存/金流細節
class VendingMachine(
    private val inventory: InventoryManager,
    private val payment: PaymentProcessor,
) {
    fun buy(code: String, cash: Money): Purchase {
        require(inventory.isAvailable(code)) { "out of stock" }
        val product = inventory.dispense(code)
        val change = payment.charge(product.price, cash)
        return Purchase(product, change)
    }
}
  • 客戶端(機器的軟/硬體介面)只看到 VendingMachine 一個簡單入口——這正是 Facade 的價值:為複雜子系統提供統一介面
  • 每個子系統各守一個職責(SRP):庫存、金流、交易資料分屬不同類別。
  • 書中明講:為避免 VendingMachine 淪為 god object,facade 應保持輕量、把任務委派出去
設計取捨(書中的 Design Choice)
  • 職責歸屬Product 連結 Rack(物理位置),而非反向——Product 是靜態實體,不該管自己的儲存;庫存量交給 InventoryManager(SRP)。
  • 分發不放在 RackRack 只管庫存與商品資訊,分發動作上移到 InventoryManager
  • 集合的封裝:管理 rack 集合時,優先給 addRack/removeRack 這種粒度方法,並考慮 immutable collection / defensive copy,而非直接曝露整個 map。
Facade vs 相鄰模式 + 誤用
  • Facade vs Adapter:Facade 是「簡化一組子系統的入口」(意圖:好用);Adapter 是「把既有介面轉成另一個介面」(意圖:相容)。
  • Facade vs Mediator:Facade 單向對外簡化、子系統不認得它;Mediator 讓同儕彼此透過它協調。
  • 誤用:facade 開始塞業務規則、做驗證與計算 → 它就悄悄長回 God Object。門面只協調,邏輯留在子系統。

🔑 Takeaways

✍️ My Notes

No notes yet — jot your takeaways or Q&A here.

📖 Further Reading