今天來聊個最近很夯的主題 DDD,但不是 DDD 的本尊 Domain Driven Design,而是無所不在的 Database Driven Design,試著回想一下,平常在開發一個新功能時,是不是有以下症狀:
如果有以上症狀,那有很高的機率是 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 和 Figure 2 可以看到兩者的差異,事實上,Merchant
物件需不需要有一個 orders
的陣列,代表某個商家的所有訂單,或是 Order
有個 merchant
指向所屬的商家?若是使用 ORM 框架,框架會建議你要加,但這是因為 ORM 框架需要而不是你的 Domain Model 需要。
因此,從 database schema 出發,往往會影響到 OO 的模型設計,但有趣的是,自己的經驗是,先從 OO 模型出發,反而不太會影響到 database schema 的設計。
再來,個人覺得最大的問題是,都沒有再討論行為,都只討論狀態,OO 的重點是把狀態封裝,透過有限的行為操作物件,OO 的模型怎麼可能會沒有行為?在閒談軟體設計:State 與語言中,在建立 SlotMachine
介面時,都是以自動販賣機能提供什麼行為開始,內部的狀態反倒是其次。這邊提供兩種不同的實作範例,第一種用狀態的角度出發進行設計 (解說用,某些內容省略,不能編譯):
第二種則是從可以有什麼操作的角度出發進行設計:
不知道大家比較喜歡那個版本?我覺得寫程式沒有標準答案,所以僅提供幾個觀點讓大家思考:
PUT /orders/{merchantId}/{orderId}/state
比較直覺?還是 PUT /orders/{merchantId}/{orderId}/accepted
比較直覺?UpdateOrderStateRequest
也把取消訂單需要的內容也放進去時,說明文件好寫嗎?OrderManager
還是 Order
?Order
還是沒有邏輯的貧血模型嗎?基於上述的觀點,我個人喜歡第二個版本。當然,這是隱含著使用 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
呢?
這一樣沒有標準答案,同樣提供幾個觀點去思考:
SuperMerchant
或是個別的物件中,哪個比較直覺?OrderService
時,取得一個 SuperMerchant
或是 OrderSetting
,哪個比較直覺?當然也有可能 SimpleMerchant
和 OrderSetting
都需要的情況。雖然說讓 Database Driven Design 背這個鍋其實有點尷尬,只是跟貧血模型的症狀很像,自己的觀察,Database Driven Design 好像特別容易會變成這樣。
好啦,來總結一下,如果正在嘗試用 DDD 開發應用程式,也許可以思考一下,是哪一種 DDD?有以上幾個症狀嗎?