iT邦幫忙

2024 iThome 鐵人賽

DAY 24
1
Software Development

一個好的系統之好維護基本篇 ( 馬克版 )系列 第 24

Day-24: Domain Driven Design 與 API 設計的難處

  • 分享至 

  • xImage
  •  

https://ithelp.ithome.com.tw/upload/images/20241008/200893589s8XXzQKu6.png


上一篇文章我們談完 Domain Event 的發送部份以後,接下來我這篇文章想要談談 API + DDD,會有這篇文章是因為我在實作時有碰到一些 API 設計的問題,例如 :

  • API 回傳的結構是要以 Domain Model 為主嗎 ?
  • 但是如果 Domain Model 裡面沒有的東西要如何處理 ?
  • 前端的畫面如果要的東西,是 2 個 BC 的東西要如何 ?
  • 承上題,如果前端要的東西又要分頁怎麼辦 ?
  • 如果 Bounded Context 或 Aggregate 名稱變了,那 API 怎麼辦呢 ?

我之前一直在花時間看這個部份的東西,因為 DDD 內部實作還算蠻明確的,但是我每一次寫到 API 那一個層級腦袋都會跑出好多問題,而且好像在 DDD 的書都很少討論到 API 設計這一塊,不過這也合理因為 DDD 本來就是以 Domain Layer 為主。

接下來就開始吧。


方案 1: 每個 Bounded Context 就是有獨立的 API

https://ithelp.ithome.com.tw/upload/images/20241008/20089358bK80AzoLJJ.png

這個應該是不少人第一個直覺會是以這個方案來進行,會有什麼問題呢 ?

  1. 你的 api 如果是以 Aggregate 為單位,那如果回傳的東西沒有 Aggregate 呢 ? 或是 Aggregate 裡沒有的欄位呢 ?
  2. 你的 API Resource Path 是什麼呢 ? Bounded Context 改名了那如何處理呢 ?

接下來我們一個一個回答。

1. 你的 api 如果是以 Aggregate 為單位,那如果回傳的東西沒有 Aggregate 呢 ? 或是 Aggregate 裡沒有的欄位呢 ?

首先這裡我認為,如果要使用這個方案的話,那至少在 Controller 那一層一定要將 Aggregate 轉成 ViewModel 概念的東西,這樣我們就不會因為 UI 要啥,然後修改了 Aggregate。然後這個 api resource 要以 Aggregate 名稱來叫嗎 ? 我覺得可以。

接下來是如果是沒有 Aggregate 的情況下呢 ? 我自已在實務上碰過不少是 UI ,需要統計資訊的情況下,所以這裡很會有兩種解法 :

  1. 如果只是要 Aggregate 的統計資料,那很簡單,就是開個 AggregateStatsic 之類的 api。
  2. 如果要的統計資料是你負責的 Aggregate 加上其它不是你的 BC 的 Aggregate 統計資料,那就幹自已的,你運氣好的話,但如果就是要你交出,大概就是你這個 BC 去抓其它 BC 的資料……,但你這個 api 要叫啥最後就變 UI 導向名稱例如 xxxPageStatisc 之類的 ?

2. 你的 API Resource Path 是什麼呢 ? Bounded Context 改名了那如何處理呢 ?

正常的情況下,不太可能一有 Boundd Context 就直接切微服務,我自已覺得那個不太好,BC 本質是給你獨立的單位,但不代表要切,因為微服務是有成本的,先排除掉溝通這一塊 ( 因為 BC 就是為了獨立 ),但是你還是要有維護的成本,而且運氣不好某個人又用什麼 c 語言寫,你真的會想打人。

所以正常情況下就是只是在同個 repo,然後根據 BC 切資料夾,然後這時 API Resource Path 會叫啥 ?

/{bounded context}/{resource} ? 但問題就在於你 context 重新命名與重構了怎麼辦 ?

3. 很多時後,你的 UI 不是根據你的 Bounded Context 來決定有什麼資料,而是使用者需要,那這時有個列表需要的,不是你現在這個 BC 資料怎麼辦 ?

比較好的解法是請前端打兩隻。但很容易發現生另一個 BC 是其它團隊的服務,而且還需要一些權限驗證,那這時通常也只能寫在這個 BC 裡了。

這個方案的總結

這個模式有個很大的問題就在於,你的 BC 很容易變成支援某個 UI 的東西,這樣就很容易導致前端不是根據 BC 職責來呼叫你,而是有什麼資料就呼叫你。想想這個情境,我們前端通常在找 API 時,很常先根據畫面上看發現,有這個他需要的欄位,然後就去找用的 API,然後如果這個 API 有提供,那就直接拿來用,對吧 ?

所以這個方案很容變成:

你的 Bounded Context 最後會變成不知道他實際提供那些資料,也不知道資料的職責

還有另一個問題就是:

你要 Bounded Context 重構怎麼辦呢 ?


方案 2. 有一個 BFF ( Backend For Frontend ) 或是 For 前台的 Bounded Context

https://ithelp.ithome.com.tw/upload/images/20241008/20089358G2EjbJ30W2.png

然後上面整個問題的原因,我覺得是:

UI 與 Bounded Context 是沒綁定的

理想上,一個 BC 就是一個團隊的地盤,相對的 UI 希望也是,例如我們在 event storming 討論的 read model 本質上就是 UI 的表現,但是問題就在於:

我們實務上也很難用 Bounded Context 來限制 UI 的顯示啥,因為 UI 是用戶導向

所以這裡我們嘗試了使用 Backend For Frontend 的架構來嘗試解決這件事情,但是我們碰到好多的坑如下:

  1. 跨 BC 分頁難題
  2. 大表資料內容下載問題
  3. 權限到底要寫那 ?
  4. 檔案傳輸的問題
  5. Command 情況下的 API 很常要重工

1. 跨 BC 分頁難題

這題真的難,首先整個最大的問題在於 :

所有的分頁一定是要資料庫中處理

然後先說一下,我這裡分兩個情境下來說:

  1. 有切 BC 但資料庫還是同一個
  2. 有切 BC 資料庫已分

理想上每個 BC 都是有獨立的資料庫這個比較合理,但很多情況下資料庫的切分是真的難,所以通常會是先在 application 層開始整理地盤,然後慢慢的根據 BC 切出那個它們地盤需要用的表後,才有可能分資料庫。

然後第 1 種情況下,我們最後還是要走回,再某一個 BC 下,下資料表的 JOIN、SORT 分頁,最後再從這個 BC 回傳給 BFF,然後再回給前端。這樣就又變成 BC 回傳的資料不一定是這個 BC 職責的。

第 2 種情況下應該就是建立 data lake,然後再去這個地方來抓資料,但是你想想每一次開發時,你都需要做這些事情,才能完成一個簡單的 table,會不會想罵髒話。

這題目前真的有點無解,我們是走 1,然後我腦袋一直在想這是不是怪怪的。

2. 大表資料內容下載問題

就是我們通常有個功能就是列表上有個下載,然後他是下載所有資料的,不會有分頁,然後這裡就發現好慢啊,因為它 network latency 很高,因為它中間多了一層。

這裡我們最後變成這樣 :

  1. 前端發送請求給 BFF
  2. 進入非同步的時間
  3. BFF 請求到 BC
  4. BC 傳資料到 BFF
  5. BFF 送資料到 GCS,然後產生下載連結。
  6. 最後主動通知前端說好了,並且回傳下載連結 ( websocket or sse )

雖然一樣很久,但就是別卡住用戶操作就都還好。

順到說明一下,為什麼不要在 BC 產 GCS 連結,不是說不行,但很多情況下是因為 BC 不是你管理。

https://ithelp.ithome.com.tw/upload/images/20241008/20089358Fzj8F0WRd6.png

3. 權限到底要寫那 ?

這題我事實上思考很久,主要的糾結點在於:

  1. 我驗證的遊戲規則與資料都在 BC 內,但矛盾的是權限是用在 UI 上的
  2. 但是有些 command 類的權限就不算綁 UI,這種寫在 Aggregate 我又覺得很合理 ( 例如用戶是創作者的情況下,才能執行課程修改之類的 )

然後我們這裡是用 RBAC 所以這裡我整理一下情境:

  • Query: 這種很看 UI 什麼地方可以看,什麼地方不行,那這個寫在 BFF 我覺得合理。
  • Command : 這種很吃 domain 的,我覺得寫在 Aggregate 也合理,因為看就知道這個業務誰可以操作。

但最後就變成 BFF 有綁 RBAC 的權限,然後在 Aggregate 裡面我們會在進行更詳細業務權限判斷 ( 例如這個操作要管理者並且有寫個評價的才能操作 )。

這個參考就好,我不覺得這是最佳解。

4. 檔案傳輸的問題

就是我們有時後不是會實作影片、圖片、或啥的上傳功能嗎 ? 影片還好,因為實務上我們通常不是直接傳影片到 application 再到 video 服務去,通常是前端會和後端要到上傳位置以後,就由前端上傳到那個位置,目前這個應該是所有影片服務商的正規流程。

https://ithelp.ithome.com.tw/upload/images/20241008/20089358P2cvPFrynl.png

然後接下來就是圖片、檔案的上傳,很多情況下我們要轉換,也就是說 client 先到 bff,再傳到 bc,雖然量小還好,但是我們的流量消耗要花 2 倍,積少成多,最後就發現這麼雲端網路費用變高了……

https://ithelp.ithome.com.tw/upload/images/20241008/20089358eN0JHJN0QA.png

這裡我自已覺得比較好的解法也是和 video 用相同的方法,就是 client 是直接存到 storage。

5. Command 情況下的 API 很常要重工

因為 BFF 的本質上就是個為了支援 UI 的 Query 集中器,所以每當我們寫 command 相關的 api 時,都會發現我們只是將請求、包含內容直接丟到後面的 BC 中,聽起來還好,但煩的點是:

  • 做這件事好像沒啥用處,就是多工。
  • BFF 我們也是要寫測試。
  • 而且我們的 Input 驗證也不是寫在 BFF ( 它是會有,但就是基本的,不會有業務驗證 ),因為寫了如果和 BC 那不一致就打架了。

這題我有在考慮,就直接將 bypass 給 bc 就好,剩下就交給他處理,但我還沒仔細想想有沒有什麼問題,就單純先說說幹話。

6. 這個 BFF 是誰管 ? 如各 BC 都有不同團隊的情況 ?

以這種情況下,各 BC 事實上都會可以去修改這個 BFF ( 有錢的公司例外,因為說不定每個都是一個團隊 )

7. 如果有 BFF 那是不是如果一個 command 操作是需要操作三個 BC 的,那是不是這裡打那三個 BC 就好 ?

對吧 ? 例如建立訂單的情境就可能如下:

  • Purchase BC: 處理訂單結帳
  • 分潤 BC: 處理老師課程分潤
  • 第三方分潤 BC: 就是要分潤給其它第三方導購分潤

那是不是我們就可以在 BFF 呼叫這幾個 BC 的 API,那這個問題就在於 :

Domain Event 還需要嗎 ?


小結

整體而言,理想上我是會希望走 :

第一個方案 + UI 是有對應到 Bounded Context

我以前在 kkbox 一起聽時,我自已覺得是有符合這個方案在走,開發起來真的很舒服,不會碰到 BFF 的坑,也很少需要到去抓其它服務資料,有需要也只有一點點 ~ 理想啊 ~

但是如果現在要叫訂個規則和 UI/UX 說我也不確定要如何定呢 ? 總結後我會開始覺得走第一條路,然後就讓 BC 有 UI 的東西,好像會比較好點,不過這個是我的想法,我現在也沒太好的解法 ~ 就把我碰過的問題列出來討論看看 ~


上一篇
Day-23: Domain Event 之 Transactional OutBox 與 EventBus
下一篇
Day-25: 如何設計與管理 Bounded Context
系列文
一個好的系統之好維護基本篇 ( 馬克版 )30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言