2024-03-09|閱讀時間 ‧ 約 29 分鐘

閒談軟體設計:Database Driven Design?


今天來聊個最近很夯的主題 DDD,但不是 DDD 的本尊 Domain Driven Design,而是無所不在的 Database Driven Design,試著回想一下,平常在開發一個新功能時,是不是有以下症狀:

  • 總是先從 database schema 開始設計,連 use case 都還沒討論清楚,就已經先有 schema 了
  • 討論 use case 時,都是針對 database schema 的 CRUD,連 API 看起來都像是某個 table 的 CRUD
  • 除了 service 物件,物件都只有資料,沒有 getter/setter 以外的邏輯 (貧血模型)
  • 不太考慮 boundary context,把不同服務所需要的資訊都放在一個大表中,於是形成一個超大物件。


如果有以上症狀,那有很高的機率是 Database Driven Design,這邊要先聲明,這種設計方式不見得是不好的,如果是簡單的應用程式,只需 CRUD 卻硬要套複雜的 Clean Architecture 或是 Domain Driven Design 有時反而沒有必要,使用某些現成的框架,快速地完成 CRUD 的開發是可以接受的。

在討論 Database Driven Design 可能遇到的問題前,先閒聊一下個人過去的學習經驗吧!大二算是正式接觸物件導向設計,在那之前都是自學,從各種不同的管道東拼西湊關於物件的概念,可能是學校課程太過偏於語言怎麼實作物件導向的特性,於是那門課上完後,除封裝、繼承與多型,大多記得的是語言特性 (C++),像是 class、pointer、overloading、pure virtual function,實際上怎麼用這些東西設計出真正的物件導向系統,倒是沒什麼概念。

大三學資料庫系統,從正規化開始,什麼第一正規化、第二正規化到第三正規化,ER 模型,一對多、多對多、reference key 等等,最後專題用 PHP 寫個組裝電腦用的購物車 (跟原價屋有點像,但會幫忙過濾掉不相容的規格,例如選 Intel 的 CPU 會過濾掉 AMD 晶片組的主機板之類的),那時就真的是 Database Driven Design 了,物件導向完全沒用上,但還是弄出個可以動的系統。

碩一的物件導向設計課,學 design pattern,整個學期分七次作業慢慢寫出一個 class diagram 編輯器,加上作業要求用上特定的 design pattern,慢慢開始對物件導向有感覺了。接著修 OOAD,從 use case 開始分析模型,用在自己的論文上,開發 visual language 的編輯器,算是比較熟悉物件導向設計了。這兩個 editor 用的是自定義的檔案結構,沒有用到資料庫,因此完全不會有被 schema 綁架的問題。

好,回到正題,會寫這篇主要是進到業界後,開發系統時一定會碰上資料庫,只要碰上資料庫,總是覺得哪裡怪怪的,物件之間的關聯是為了 ORM 寫的、核心的模型卻依賴 ORM 框架,DAO 或是 repository 充斥著各種邏輯,又或者出現上述的幾個症狀。

首先,OO 模型和 ER 模型是不一樣的,OO 有繼承、實作、composite、aggregate 等不同關係,不是每一個都能一對一的對應到 ER 模型,ER 模型為了處理多對多的關係,會有 relation table,但 OO 不需要,ER 需要 reference key,或是因應 multi-tenancy 設計所加的 columns,但這些資料作為物件的 field 卻不一定有用。例如,一個多商家使用的電商系統,在訂單資料表上一定會有像是 merchant_id 的 column,但訂單物件卻不一定要有 merchantId 的 filed。


Figure 1 - ER Model


Figure 2 - Class Diagram


從 Figure 1 和 Figure 2 可以看到兩者的差異,事實上,Merchant 物件需不需要有一個 orders 的陣列,代表某個商家的所有訂單,或是 Order 有個 merchant 指向所屬的商家?若是使用 ORM 框架,框架會建議你要加,但這是因為 ORM 框架需要而不是你的 Domain Model 需要。

因此,從 database schema 出發,往往會影響到 OO 的模型設計,但有趣的是,自己的經驗是,先從 OO 模型出發,反而不太會影響到 database schema 的設計。

再來,個人覺得最大的問題是,都沒有再討論行為,都只討論狀態,OO 的重點是把狀態封裝,透過有限的行為操作物件,OO 的模型怎麼可能會沒有行為?在閒談軟體設計:State 與語言中,在建立 SlotMachine 介面時,都是以自動販賣機能提供什麼行為開始,內部的狀態反倒是其次。這邊提供兩種不同的實作範例,第一種用狀態的角度出發進行設計 (解說用,某些內容省略,不能編譯):


第二種則是從可以有什麼操作的角度出發進行設計:


不知道大家比較喜歡那個版本?我覺得寫程式沒有標準答案,所以僅提供幾個觀點讓大家思考:

  • 當要呼叫 API 接受訂單時,是 PUT /orders/{merchantId}/{orderId}/state 比較直覺?還是 PUT /orders/{merchantId}/{orderId}/accepted 比較直覺?
  • UpdateOrderStateRequest 也把取消訂單需要的內容也放進去時,說明文件好寫嗎?
  • 能不能接單的邏輯,到底該歸 OrderManager 還是 Order
  • 比較喜歡 switch...case 的直接了當,還是清楚語意的 method name?
  • 當要修改接受訂單的邏輯時,哪個版本比較容易知道修改何處?
  • 修改完後,那個版本需要比較多的回歸測試?取消訂單要測試嗎?
  • Order 還是沒有邏輯的貧血模型嗎?
  • 物件一定需要提供 setter 嗎?

基於上述的觀點,我個人喜歡第二個版本。當然,這是隱含著使用 OO 作為設計的基礎假設,若是使用 functional programming 或是 Data-oriented programming,可能會有完全不同的想法。貧血模型不見得不好,就看團隊的喜好,會變成貧血模型,也不完全一定是因為 Database Driven Design,但自己的觀察是 Database Driven Design 確實比較容易變成貧血模型。

關於 boundary context 的症狀,單從 single responsibility 的角度來看 (參閱閒談軟體設計:Single Responsibility),一個物件承擔多種服務的設定是合適的嗎?

一個電商平台會提供多種服務,下單 (order)、支付 (payment)、物流 (logistics),不同的服務都有各自的設定,那大家會比較喜歡 Figure 3 中的 SuperMerchant 還是 SimpleMerchant 呢?


Figure 3 - SuperMerchant vs. SimpleMerchant


這一樣沒有標準答案,同樣提供幾個觀點去思考:

  • 假設設定的屬性之間有關連性,有邏輯需要驗證關聯性,例如若開啟信用卡,就必須開啟至少一種卡別,那邏輯放在 SuperMerchant 或是個別的物件中,哪個比較直覺?
  • 在實作 OrderService 時,取得一個 SuperMerchant 或是 OrderSetting,哪個比較直覺?當然也有可能 SimpleMerchantOrderSetting 都需要的情況。
  • 微服務流行了好一陣子,假設下單 (order)、支付 (payment)、物流 (logistics) 都拆分成微服務,在資料庫分離的情況下,哪個比較容易?

雖然說讓 Database Driven Design 背這個鍋其實有點尷尬,只是跟貧血模型的症狀很像,自己的觀察,Database Driven Design 好像特別容易會變成這樣。

好啦,來總結一下,如果正在嘗試用 DDD 開發應用程式,也許可以思考一下,是哪一種 DDD?有以上幾個症狀嗎?

分享至
成為作者繼續創作的動力吧!
© 2024 vocus All rights reserved.