需求:訂單成立時,要同時通知使用者(Email)、風控(SMS)、營運(Slack)。未來還會加 App push、webhook…而下訂的核心流程不該每加一個渠道就被改一次。怎麼設計?
🎯 Scenario
// 反例:下單流程硬編每一個渠道,每加一種就要回來改 placeOrder
fun placeOrder(order: Order) {
save(order)
emailClient.send(order.user.email, "...") // 每加一個渠道
smsClient.send(riskPhone, "...") // 就要動這個核心方法
slackClient.post("#ops", "...") // 違反 OCP
}
需求關鍵字:一個事件、多個接收者、接收者會增減、發布端不想認識每個接收者。這正是 Observer(發行/訂閱)的主場。
🧠 Intuition
TIP
先別看下面。問自己:這裡「會變的」是什麼?是接收者的清單(誰要被通知)。那就把「發布事件」和「如何處理事件」解耦——發布端只管喊一聲,誰在聽、聽到做什麼,交給各自訂閱者。
⚖️ Reference Design
主解法 — Observer:發布端只認得一個 Subject
fun interface OrderListener { fun onPlaced(order: Order) }
class OrderService(private val listeners: List<OrderListener>) {
fun placeOrder(order: Order) {
save(order)
listeners.forEach { it.onPlaced(order) } // 只喊一聲,不認得任何渠道
}
}
// 各渠道是獨立訂閱者;加一個 = 新增一個類別 + 註冊,核心流程不動
class EmailListener(val c: EmailClient) : OrderListener { override fun onPlaced(o: Order) { c.send(o.user.email, "...") } }
class SlackListener(val c: SlackClient) : OrderListener { override fun onPlaced(o: Order) { c.post("#ops", "...") } }
OrderService(listeners = listOf(EmailListener(email), SmsListener(sms), SlackListener(slack)))Observer 之外的搭配與抉擇
- 每個渠道『怎麼送』會變 → 渠道內部再用 Strategy(如重試策略、格式化策略)。Observer 管「通知誰」,Strategy 管「怎麼做那一步」。
- 要解耦、削峰、可靠投遞 → 把事件丟上 message queue / event bus,訂閱者非同步消費(Observer 的跨行程版本)。
- 一個 lambda 就夠、渠道固定不變 → 別急著開一堆類別,先 YAGNI;等「會增減」這個訊號真的出現再上 Observer。
常見誤用
- 在監聽者裡塞業務決策:訂閱者只做「把這件事送出去」,不要在裡面又改訂單狀態、觸發連鎖——否則通知順序變成隱性流程依賴。
- 同步通知阻塞主流程:某渠道很慢/會失敗時,同步
forEach會拖垮下單。需要時改非同步或加隔離。 - 忘了取消訂閱:長生命週期的 subject 持有短命訂閱者的引用會造成洩漏(lapsed listener)。
🔑 Takeaways
- 「一事件 → 多接收者、且接收者會增減」→ 用 Observer,發布端只認得訂閱介面、不認得渠道,滿足 OCP。
- 渠道內部「怎麼送」的變化用 Strategy 收;要可靠/削峰改走 message queue。
- 監聽者只負責送出、別塞業務決策;注意同步阻塞與訂閱洩漏。
No notes yet — jot your takeaways or Q&A here.