以往在 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 架構下,實現了這些需求不難,但實際上,隨著業務需求的增加,在我們的 ControllerService 其實會越來越肥大,如果要做更精細的分層,可以解決肥大,但也會造成 耦合度 過高的問題。

參考 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 的好處,小弟認為也能增加維護性,也能讓程式碼更有彈性。