iT邦幫忙

2024 iThome 鐵人賽

DAY 15
0
Software Development

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

Day-15: Domain Model 實務上面對的困境之 DDD Trilemma

  • 分享至 

  • xImage
  •  

https://ithelp.ithome.com.tw/upload/images/20240929/20089358XDtkr96oZi.png

上一篇文章我們有提到 domain model 以後,接下來我們可以來說說 DDD Trilemma,這個東西就是主要在說 domain model 設計上的 Trilemma,然後最早提到這個問題的應該是從這篇文章來的。

Domain model purity vs. domain model completeness (DDD Trilemma)

接下來就來談談這個東西 ~ 因為這個有個東西很容易會讓domain model 變歪呢


什麼是 DDD Trilemma 呢 ?

首先三難困境的三種特性 :

  • Domain Model Completeness: 不要有 domain logic fragmentation。也就是說有些 domain logic 在其它 layer,而沒有集中,那就是有 fragmentation。
  • Domain Model Purity: domain model 不會依賴任何的 out-of-process 服務。
  • Performance: 性能,下面的範例可能會讓你更好理解。

然後三難困境就是在說 :

任何一種實作方法,都只能達成以上兩種,不可能三種都有。


三難困境範例

我們先假設一個需求情境 :

假設我們有個需求是用戶註冊,然後需要檢查服務中有沒有已經存在的 email 了

然後根據文中會有三種寫法:

  1. Push all external reads and writes to the edges of a business operation
  2. Inject out-of-process dependencies into the domain model.
  3. Split the decision-making process between the domain layer and controllers.

https://ithelp.ithome.com.tw/upload/images/20240929/20089358IAlQHiu9tP.png
圖片來源: Domain model purity vs. domain model completeness (DDD Trilemma)

第一種寫法 : 將所有 email 抓出來,然後放在 domain model 中判斷 ( Push all external reads and writes to the edges of a business operation )

這種就是會先將所有的 email 拿出來,然後丟到 domain model 中來做業務判斷。然後根據文中它符合了以下兩點 :

  • ( V ) domain model completeness: 因為他將判斷的地方拉到 domain layer 的地方,所以沒有 domain logic fragmentation。
  • ( V ) domain model purity: domain model 沒有依賴 out-of-process 的東西。
  • ( X ) performance: 因為我們需要將所有 email 拉出來,所以很浪費資源。
// Service
class UserService {
  constructor(private repository: UserRepository) {}

  async registerUser(newEmail: string) {
    const existEmails = repository.getEmails();
  
    const user = new User();
    user.register(newEmail, existEmails);
    this.repository.save(user);
  }
}

// Domain Model
class User {
  constructor() {}

  register(newEmail: string, existEmails: string[]) {
    if(existEmails.include(newEmail)){
       throw new Error('the email is existent')
    }

    this.email = newEmail;
  }
}

第二種寫法: 讓 domain model 呼叫 repository ( Inject out-of-process dependencies into the domain model )

  • ( V ) domain model completeness: 因為他將判斷的地方拉到 domain layer 的地方,所以沒有 domain logic fragmentation。
  • ( X ) domain model purity: domain model 中呼叫了 repository (out-of-process)。
  • ( V ) performance: 直接在 db 檢查看有沒有就好。
// Service
class UserService {
  constructor(private repository: UserRepository) {}

  async registerUser(newEmail: string) {
    const user = new User();
    await user.register(newEmail, this.repository);
  }
}

// Domain Model
class User {
  constructor() {}

  async register(newEmail: string, repository: UserRepository) {
    const existingUser = await repository.getByEmail(newEmail);
    if (existingUser) {
      throw new Error('the email has existent');
    }

    this.email = newEmail;
    await repository.save(this);
  }
}

第三種: 就是將一些業務判斷拉到其它 layer ( Split the decision-making process between the domain layer and controllers )

  • ( X ) domain model completeness: 因為判斷 email 存不存在的地方拉到 service layer 了,所以產生了 domain logic fragmentation,因為不同 layer 有處理 domain logic。
  • ( V ) domain model purity: domain model 沒有呼叫 repository。
  • ( V ) performance: 直接在 db 檢查看有沒有就好。
// Service
class UserService {
  constructor(private repository: UserRepository) {}

  registerUser(newEmail: string) {
    const existingUser = repository.getByEmail(newEmail);
    if (existingUser) {
       throw new Error('the email has existent')
    }
    
    const user = new User();
    user.register(newEmail);
    repository.save(user);
  }
}

// Domain Model
class User {
  constructor() {}
  
  register(newEmail: string) {
    this.email = newEmail;
  }
}

那一個比較好 ? 第三種 !

根據上面的範例與下面這張圖應該可以得出,我們只要使用 domain model 那就不可能三個特性都全部達成,那這樣的話,那一個種比較好呢 ?

https://ithelp.ithome.com.tw/upload/images/20240929/20089358IAlQHiu9tP.png
圖片來源: Domain model purity vs. domain model completeness (DDD Trilemma)

文中作者是說 :

I strongly recommend that you choose domain model purity over domain model completeness.and go with the third approach: splitting the decision-making process between the domain layer and controllers.

就是我們第三種方案的實作 :

  • domain model purity: 也就是 domain model 不依賴任何的 out-of-process 的東西。
  • 然後會有一些判斷在 service 層 ( or controller )。

理由如下:

  • 業務整個服務的核心,如果包含了 out-of-process 的東西,那它久了就是變成一個包山包海的東西。
  • DDD 也提倡這概念,因為 Domain Logic 是整個軟體架構的心臟,它在產品導向的軟體工程只會越來越複雜。
  • Functional Programming 也提倡這個選項,因為這與它的 pure function 避免隱藏的 input 與 output ( api、db ) 概念相同。
  • Unit Testing 也選這方法,因為易與測試,domain model 可以單純的測試 domain logic。

小結

https://ithelp.ithome.com.tw/upload/images/20240929/20089358TYeStxu3s7.png

整體來說我很讚成作者給的建議,並且目前在開發上我們也都是用第三種模式來開發,主要的原因如下 :

  1. 首先我們不能拋棄性能,因為很多情況下,性能是讓用戶最體感的東西,一個好的系統沒有用戶,你維護度在高,你最後還是要喝西北風。順到說一下,性能與維護在軟體工程這個領域是互相拉扯的,不能完全走一邊,要追求的是平衡,而至於要如何抓,我自已的標準是有影響到用戶的情況下,我就會考慮做一些不考慮維護性的處理。
  2. 然後接下來 purity 與 completeness 我們也是選擇 purity,因為我們舊有的程式碼有 domain model 而且讓他取 repository 中抓資料,最後就變成 domain model 與 service 不知道那個該寫那,因為 completeness 的寫法 domain model 也可以用 db 抓資料啊……

軟體工程中性能與維護這個是個兩難與 Tradeoff,這篇文章很明顯的表達了這件事情。

對了 ~ 但我有個情況下,會在 domain model 在呼叫 out-of-process 的東西,因為我發現 domain model 裡的所有欄位變化,都很依賴那個,也就是沒有呼叫,那我 domain model 事實上沒什麼用,所有東西都是從外部丟進來的。

別忘了 domain model 是為了解決業務複雜變化的工具,如果有個 domain model 真很吃 out-of-process 的情況下,我覺得 ok ~ 我記得那時是在設計第三方金流服務的 payment domain model,我那時最初也是 purity 的寫法,但寫完後發現,我要這個 domain model 要幹啥呢 ? 所有變化與欄位都需要的東西都要從外部金流帶入呢 ~


上一篇
Day-14: 提升維護性與降低複雜度的好方法之 Domain Model
下一篇
Day-16: DI 是什麼?深入探討 IoC、DIP 的關聯性與好處
系列文
一個好的系統之好維護基本篇 ( 馬克版 )30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言