為 Pattern 而 Pattern

看完 GoF 之後,看什麼都想套一個 Pattern。UserService 需要一個 UserServiceFactory,但 UserService 從來只有一種實作——這個 Factory 什麼問題都沒解決,只是增加了一層間接。

Pattern 存在的理由是解決一個具體的設計問題——靈活性、擴展性、解耦。如果問題不存在,Pattern 就不應該存在。


Singleton 到處塞

Singleton 是最常被濫用的 Pattern,因為它用起來最簡單(一個 static 方法就搞定)。

常見的 Singleton 誤用:把整個 Database ConnectionConfigUserRepository 都做成 Singleton,然後在 class 裡直接 Database.getInstance() 存取。

問題:

  • 測試無法替換:測試時無法用 fake 替換,必須連真實 DB
  • 隱藏依賴UserService 的依賴不在建構子裡,看 class 定義看不出來它依賴什麼
  • Global state:多個 Singleton 互相依賴,初始化順序複雜

修法:DI(Dependency Injection)——把依賴顯式傳入,DI Container 管理 singleton 的生命週期。


把 Strategy 和 State 搞混

兩個 Pattern 都用 interface + 多態替換行為,但解決的問題不同:

  • Strategy外部選擇算法(排序用快排還是合併排序,由呼叫者決定),物件本身不知道也不在乎
  • State物件自己根據狀態改變行為(訂單在不同狀態下,cancel() 的行為不同),狀態轉換是物件內部邏輯

把 State 的 transition logic 放到外部(Strategy 的做法),讓呼叫者決定什麼時候切換狀態,違反了封裝。


Builder 用在簡單物件

Builder Pattern 解決的問題是:有很多可選參數、建構過程複雜、需要保證物件完整性。

// 這種 Builder 是純粹的繁複
Order order = new OrderBuilder()
    .setId(1)
    .setProduct(product)
    .build();
// 等同於:new Order(1, product)

三個以下的參數,用建構子或工廠方法就夠。Builder 適合參數超過 5 個、多個可選參數、或建構步驟有驗證邏輯的場景。


Observer 沒有 Unsubscribe,記憶體洩漏

// 常見的記憶體洩漏
class UserPage extends Component {
    componentDidMount() {
        eventBus.on('user:updated', this.handleUpdate)
        // 忘記在 componentWillUnmount 呼叫 off()
    }
    // ❌ 沒有 unsubscribe
}

Component 被 unmount 後,事件系統仍然持有 handler 的引用,導致 Component 無法被 GC,而且 handleUpdate 還可能在一個已被銷毀的 component 上執行。

修法:每個 on() 對應一個 off(),用 WeakReference 或語言的自動清理機制(React useEffect 的 cleanup function)。


Proxy 和 Decorator 混淆

兩個 Pattern 的結構相似(都有一個 wrapper 包住真實物件),但語意不同:

  • Proxy:控制對真實物件的存取——lazy initialization、access control、logging、remote proxy
  • Decorator:動態增加真實物件的功能——在不修改類別的情況下增加行為

當你想要「限制誰能呼叫」用 Proxy,當你想要「在呼叫前後加東西」用 Decorator。把存取控制放在 Decorator 裡、把功能增加放在 Proxy 裡——功能上可能一樣能跑,但語意混淆讓維護者困惑。


Pattern 的第一原則

Pattern 是問題的解法,不是程式碼的裝飾。

識別問題 → 找匹配的 Pattern,而不是學了 Pattern → 找問題套。這個順序反了,Pattern 就成了反效果。