DDD 中的 Pub/Sub 模式應用
以往在 MVC
架構下的設計,並沒有分領域的概念,所以時常修改或新增需求時,我們都要從 API
開始 Trace..,依序地找到 Controller -> Service ...
等等,這樣的設計方式,耦合比較高之外,也不易擴充。
先從 MVC 寫個範例
今天小明想做個購物車平台,於是用了某框架的 簡單 MVC 架構開發了下單流程。
const orderController = {
createOrder: async (req, res) => {
const { userId, productIds } = req.body;
const user = await getByToken(req);
const products = await getProducts(productIds);
// createOrder
const order = await createOrder(user, products);
res.json(order);
},
};
遇到的問題
這家公司隨著規模的擴大,下單需求也不僅僅是這樣了,現在業務接了新的需求,希望訂單可以在建立之後,發送Email給使用者
。
ok…這點小需求,不算點什麼,上手!
const orderController = {
createOrder: async (req, res) => {
const { userId, productIds } = req.body;
const user = await getByToken(req);
const products = await getProducts(productIds);
// createOrder
const order = await createOrder(user, products);
// 加這行不就好了嗎?
await SendEmail(user.email, "訂單建立成功");
res.json(order);
},
};
雖然是個小問題,在一小時內就能解決,但現在公司規模擴大成五個業務了,一次接回來的問題也變多了,現在不只希望要發 Email,也希望發 Slack, Line, Telegram…等等,這樣的需求…
思考一下
確實在簡單的 MVC 架構下,實現了這些需求不難,但實際上,隨著業務需求的增加,在我們的 Controller
或 Service
其實會越來越肥大,如果要做更精細的分層,可以解決肥大,但也會造成 耦合度
過高的問題。
參考 DDD 的做法
在 DDD 之中,我們把不同領域的模型都分開了,這樣的好處是,我們可以針對不同的領域做不同的設計,不互相影響外,我們也能迅速地找到程式邏輯在哪!
const orderController = {
createOrder: async (req, res) => {
const { userId, productIds } = req.body;
const user = await getByToken(req);
const products = await getProducts(productIds);
// createOrder
const order = await createOrder(user, products);
// 這裡不能再處理Email了,Email不屬於Order領域啊!
// await SendEmail(user.email, "訂單建立成功");
// 沒錯就是把這事件給Publish出去,讓對這個事件有興趣的人來處理
await Publish("OrderCreated", order);
res.json(order);
},
};
const OrderCreatedEmailNotificationHandler = {
handle: async (order) => {
await SendEmail(order.user.email, "訂單建立成功");
},
};
我們還可以添加其他的 Handler
之外,也不必在動到原本的 Controller
,這樣的好處是,我們可以針對不同的事件,做不同的處理,而不會互相影響。
最後我們實作一下 Pub/Sub 的底層邏輯
const subscribers = {};
const setSubscriber = (event, handler) => {
if (!subscribers[event]) {
subscribers[event] = [];
}
subscribers[event].push(handler);
};
const Publish = async (event, data) => {
if (!subscribers[event]) {
return;
}
for (const handler of subscribers[event]) {
await handler.handle(data);
}
};
結論
這些想法上是剛好因為看到 Event-Driven
的書籍,才發現其實公司內部的程式碼很多也都可以照著這樣做優化,有點像是 Rails 上的 Queue
一樣,把不同的事件丟到 Queue 裡面,讓不同的 Worker
去處理。不用在找程式碼在哪,做新增或修改,只要針對事件作監聽並實作 Handler 的好處,小弟認為也能增加維護性,也能讓程式碼更有彈性。