現今的系統不論是 B to B、B to C 或是 B to B to C,通知都是不可少,不管是簡訊發送 OTP,還是發送臨時密碼的 email,或各式各樣的 push 通知,通知已不可少的環節,這也是為什麼在一開始系統架構設計時,早早把 ncc 規劃成一個獨立模組 (子系統)。
ncc 的全名是 Notification and Communication Center,乍看 Notification 和 Communication 意思好像很接近,但在語意上,Communication 泛指底層與外部系統的互動上,Nottification 則是系統與使用者的互動上。整體來說,ncc 子系統有幾個目的:
- 提供簡單的抽象層,讓其它子系統表達「通知」的意圖即可,不用處理底層複雜的「通訊」細節
- 管道式的處理流程,將不同通訊協定分開處理
- 管理通知與訂閱
- 提升通知的穩定性
意圖
很多時候,載入一個 sms 的套件或是 email 的套件,其「意圖」都是想要通知對方,sms 或是 email 的套件,只是實現這個意圖的手段,但為了這個手段卻讓商業邏輯與這些技術細節綁死,如果哪天換了個簡訊商 (別說這不可能,實際上我就遇過幾次),要換掉這些套件可不是件小工程。
那為 sms 提供一個抽象層,載入抽象層而不是某個特定簡訊商的套件,不就解決了?某種程度確實解決了一部分問題,但如果通訊的意圖更加複雜呢?例如 sms 如果失敗,換成 email?那這個轉換的細節也要放在商業邏輯中?
也許更好的方式,是讓商業邏輯層闡述其意圖即可,例如要發送簡訊,只要短短的一行:
notifications.notify(smsIntent(phone, "Your OTP is 985632."));
剩下的事就讓 ncc 去處理。下圖是簡化過後 ncc 意圖處理器的示意圖,可以發現到對商業邏輯層來說,不需要知道 SMS/eMail Provider (第三方的通訊商) 是誰,也不需要知道通知中心裡的事件是怎麼儲存的,當然也不用知道 WebSocket 或是 long polling 是怎麼處理的,只要表達意圖即可。

NCC Intent Processor
管道
從上圖可以看到,Facade 將意圖包裝後塞進 Message Queue,意圖處理器收到 Message 後,根據渠道 (Channel) 分送對應的管道 (Pipeline) 去處理,每個管道都是由一個 ChannelProcessor 處理意圖。
通常在處理這類呼叫第三方服務,都要面臨一個問題,失敗時要不要重試?如果要,要重試幾次?為了不要重複地寫這些邏輯,將重試也包裝成一個處理器 RetriableChannelProcessor,搭配 ChannelProcessorChain,將多個處理器串成一個 Pipeline,整個邏輯就清楚很多,可以任意組裝與調整。

NCC Intent Processor Classes
從下面的範例就可以看到,sms 和 email 會最多重試 2 次,push 則是由三個處理器串起來,分別處理事件儲存、呼叫 FCM 和處理 fan out。任何一個渠道想增加邏輯,大多數情況下就是寫一個處理器,串到既有的管道即可。
訂閱
雖然 FCM (Firebase Cloud Messenging) 簡化了跨平台 (iOS/Android/Web) push notification 的處理,但 FCM 只處理了最末端的取得 token 與發送訊息,一個訂閱者有多個裝置,一個裝置可能切換不同的商家,一個商家可能有多個訂閱者,還要能檢視過去的通知等等,這些都是會根據自身的服務有不同的管理方式。
在既有已知的需求下 (時常提醒自己避免陷入 over design),要檢視過去的通知,都是以一個收件匣 (NotificationEventInbox) 為單位,同樣每則 push 通知的目的地也是一個收件匣,要訂閱,也是以收件匣為單位訂閱,一個訂閱可以有多個裝置並對應到一個 token,每個訂閱會管理最後讀取的事件,可以跨裝置處理未讀的數量。

Notification Event & Inbox
穩定性
一個穩定的子系統是由多個不同面相組合起來的,像前面提到的重試也是一種面向,但只靠重試是不夠的,在雲端的環境,一個 pod 可能因為 crash 重啟,也可能因為升版重啟,那重啟的過程中,當有意圖要通知時怎麼辦?
這就是 Facade 和處理器之間用 Message Queue 的原因了,發送意圖時,不需要等待處理器是 ready 的狀態,只要確保意圖被送進 Message Queue 即可,那 Message Queue 會重啟嗎?當然可能會,因此這個部分就不是自架的服務,直接使用平台代管的 Message Queue 服務,平台代管服務通常會處理這些重啟中的請求,雖然不是滿分的答案,但依舊是可行的方案。
未來的擴充可能
目前,ncc 子系統運作十分良好,但這是在還沒有巨大壓力下的表現,依照過去的經驗,當使用者人數每成長一個量級 (10x),通訊相關的系統通常都需要一定程度的優化,在可預期的未來,ncc 應該會迎來幾個擴充和優化:
- 平行化:當意圖的數量增加,為了更高的即時性,平行化是有必較的,在目前的架構下,平行化可以是內部透過 thread pool 完成,也可以是外部增加 pod 數量來完成。
- 客製與多國語系:目前 push 通知的多國語系是交由裝置端處理,但如果通知的內容允許商家客製化後,就必須將多國語系的處理移回處理器內部 (多串一級多國語系處理的 Processor),並且提供客製訊息的管理。
- 國際化:目前簡訊商只有串一家第三方服務,而這個服務商也只能發送台灣的門號,當業務拓展到其他國家時,就需要找第二個甚至第三個服務商,這時就需要根據國碼進行服務商的切換,甚至要有退路 (fallback) 機制,當某一個服務商失敗時,能切換到另一個服務商 (一樣,多串一級 Processor 處理分派,再串一級 Processor 處理 fallback)。
- 狀態更新:現在多數的情況下,意圖的發送都是非同步,不會等待結果,
NotificationIntent本身更是一個 immutable value object,要知道處理的過程,都是靠查詢日誌來完成,之後如果要新增狀態的紀錄,或是將結果更新回商業邏輯層,都可以用新增 Processor 的方式處理。
小節
初期投資在 ncc 的架構,從構思到實作,前前後後大概一個禮拜不到,但這項投資在後面其實節省了許多時間,中間有些小補小修,但個人認為是一項十分值得的投資,更重要的是沒有過度投資,在未來擴充的那些項目,通通都沒有動工,因為目前不需要,等到需要時再進行即可。至於,這個設計適不適用在其他系統,建議還是個案討論會比較合適。














