同一事件要推到 Email/SMS/Slack,還會一直加,怎麼設計?

Medium★★★★★Studied

FromDesign Patterns (GoF)大話設計模式

Concepts practiced🔁 觀察者 (Observer)🔁 策略 (Strategy)📏 開放封閉原則 (OCP)

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

📚 From the Books

同一事件要推到 Email/SMS/Slack,還會一直加,怎麼設計?ProblemMedium設計情境

需求:訂單成立時,要同時通知使用者(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)))
  • 發布端與接收端解耦:OrderService 對「有幾個渠道」一無所知——滿足 OCP,加渠道只新增訂閱者。
  • 這也是事件驅動的核心:先註冊、後通知。
Observer 之外的搭配與抉擇
  • 每個渠道『怎麼送』會變 → 渠道內部再用 Strategy(如重試策略、格式化策略)。Observer 管「通知誰」,Strategy 管「怎麼做那一步」。
  • 要解耦、削峰、可靠投遞 → 把事件丟上 message queue / event bus,訂閱者非同步消費(Observer 的跨行程版本)。
  • 一個 lambda 就夠、渠道固定不變 → 別急著開一堆類別,先 YAGNI;等「會增減」這個訊號真的出現再上 Observer。
常見誤用
  • 在監聽者裡塞業務決策:訂閱者只做「把這件事送出去」,不要在裡面又改訂單狀態、觸發連鎖——否則通知順序變成隱性流程依賴。
  • 同步通知阻塞主流程:某渠道很慢/會失敗時,同步 forEach 會拖垮下單。需要時改非同步或加隔離。
  • 忘了取消訂閱:長生命週期的 subject 持有短命訂閱者的引用會造成洩漏(lapsed listener)。

🔑 Takeaways

✍️ My Notes

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

📖 Further Reading