# DDD (Domain-Driven Design) 1/10 ## 前言 本篇是以讀 ***产品代码都给你看了,可别再说不会DDD*** (https://docs.mryqr.com/ddd-introduction/),進行介紹。有興趣可以自行查看原文。先從一個單一的產品程式了解DDD 如何運行,後續再進行理解如何與微服務進行串接。 ## DDD入門 * 什麼是DDD? 簡單來說DDD為 物件導向設計的延伸,或是可以說是更進階物件導向設計。從定義上來說,DDD是一個整體的軟體設計方法,主要目的是著重於(面向業務)領域的軟體建模方針。 面向業務可以解讀成開發時,要視領域為一個整體進行設計,對於領域內部實現會參考業務專家的需求以及意見設計符合領域的範疇。 下面簡單幾個範例,理解一下演進: 1. 事務腳本 我們大概都寫過下面這樣的範例: ```java @Transactional public void updateMobileNumber(String mobileNumber, String memberId) { String sql = "UPDATE REGISTER SET MOBILE_NUMBER = ? , "+ "UPDATED_TIME = sysdate WHERE ID = ?;"; jdbcTemplate.update(sql, mobileNumber, memberId); } ``` 這類直接更新DB的做法有個特別的專有名詞 - **事務腳本(Transational Script)**,其處理方式簡單粗暴,但卻一點使用軟體建模的概念都沒有,在早期程式中大多是使用於某些特定事務的Utils或是一些不太需要建模的小功能為主。 --- 2. 貧血對象 要理解這類話題時必須先說明,本文並無特別支持哪套邏輯只忠實反映研究結果。 關於貧血/充血模式網路上有很多解讀甚至延伸,我們就以較常見的SPRING模式來解說, 首先可以簡單將程式要處理的部分拆分為兩個分類: * 行為 (程式邏輯/ 流程處理) * 狀態 (數據資料/ 變數處理儲存) 若將任一分類分離到單一物件中: * 狀態: 只有處理狀態(資料)的物件我們稱之為**貧血物件**,或是VO(Value Object)。更具體點來說當物件只有設計getter/setter以及其Element如下所示,就是所謂貧血物件。 * 行為: 通常就是我們所謂的邏輯層/服務層或是管理層。這邊各系統會根據不同需求切分層N階,但其原始概念就是要處理程式行為(邏輯)部分。 ```java @Setter @Getter public class Register implements Serializable { private static final long serialVersionUID = 4433072183659219606L; @NotBlank(message = "缺少custId參數") private String custId; @NotBlank(message = "缺少mobileNumber參數") @Size(max = 20, message = "mobileNumber長度太長") private int mobileNumber; private Date updatedTime; } ``` 將上述兩個物件相加就是今天我們所謂的**貧血模型**。 ```java ... 略 @Transactional public void setMobileNumberFromRequest(Request request) { Register register = registerRepository.findRegisterById(request.getCustID()); register.setMobileNumber(request.getMobileNumber()); register.setUpdatedTime(new Date()); registerRepository.updateRegister(register); } ``` 不知道看到上面程式範例是不是有一種很熟悉的感覺,如果你是JPA的使用者,應該不會陌生。在ORM世界理,上例算是很常見的作法,也可稱之為SQL-Driven的設計方式。 此作法好處是在處理資料庫表單內容時,可以像處理JAVA物件一樣簡單、直接。但恰恰是這種簡單直接的使用模式廣泛流行導致貧血的物件在使用上越來越主流,也與JAVA當初想像上的OOP設計原則背道而馳。 說真的,便於利用、易於理解、資料與流程抽離的設計並沒有問題,但可能與當初物件導向設計理念不同,OOP希望設計者可以將事務視為一個物件,希望所有與這個事務相關的處理都可以透過物件本身或是物件與物件互動方式來處理,這樣設計上較為直觀,而不是用流程如script般;step by step去控制程式執行的步驟(可參照例子:事務腳本)。更直白的講,這類的貧血物件說穿了依然使用的是事物腳本的設計概念,而不是基於事務去設計物件。 為何貧血物件在OOP設計有害?首先,**不好維護**,簡單來說不好找,以上例來說,可能你要先找到Register的物件然後透過IDE去找尋所有的使用地方,然後一點一點的研讀哪些是需要異動的,這其實相當繁瑣也不直覺,而且在做這件事的過程中你可能還會發現第二的問題,就是**重複撰寫差不多的程式**,同一想法可能只是存的方法不同或是Log紀錄的方式不同,可能就要在不同的Util /Services內寫差不多的程式,恐怖的是這樣的寫法還直接將業務的邏輯暴露出去(如setUpdatedTime案例),增加了物件與服務的相依性,這不違反了物件事務要盡量在物件自己內部完成-內聚性設計原則了嗎? 因此,若是希望修正此問題,我們可以透過DDD設計原則來解決貧血模型的缺陷。 --- 3. 領域物件 領域物件是與貧血物件相對立的物件設計概念,或者也可稱之為充血物件。顧名思義,領域物件的目標是希望能透過物件完整的將業務邏輯體現出來,不僅只是狀態(資料庫、數據...)更需要結合行為(業務邏輯)的部分,更簡單的說,是由領域(Domain)去驅動物件設計的設計方針。 我們來改寫一下範例: ```java //聚合根Register @Getter public class Register implements Serializable { private static final long serialVersionUID = 4433072183659219606L; @NotBlank(message = "缺少custId參數") private String custId; @NotBlank(message = "缺少mobileNumber參數") @Size(max = 20, message = "mobileNumber長度太長") private int mobileNumber; private Date updatedTime; public void setMobileNumberFromRequest(int mobileNumber) { this._doElementSet( mobileNo -> this.mobileNumber = mobileNo , mobileNumber); } public void setCustIdFromRequest(String custId) { this._doElementSet( id -> this.custId = id , custId); } //設定使用的共用流程(行為) private <T> void _doElementSet(Consumer<T[]> c, T param) { c.accept(param); this._updateTime(); } //聚合根的不變條件 private void _updateTime() { this.updatedTime = new Date(); } } ... 略 @Transactional public void setMobileNumberFromRequest(Request request) { Register register = registerRepository.findRegisterById(request.getCustID()); //register.setMobileNumber(request.getMobileNumber()); //register.setUpdatedTime(new Date()); register.setMobileNumberFromRequest(request.getMobileNumber()); registerRepository.updateRegister(register); } ``` 改寫後的程式雖然看起來有點複雜,但其實這樣的做法才較為符合OOP的原則。範例可見,Register物件不僅有對於數據處理部分,也將行為面的設計收攏其中(參考_doElementSet),這會讓我們的物件設計開始更接近DDD一些。而在DDD的規範裡,Register物件我們稱之為-**聚合根**,而更新元素就要更新時間的條件(_updateTime)則被稱之為~**聚合根的不變條件**,其觀念與相關內容後續會統一介紹。 --- * **小結** 本章簡單以三個範例呈現DDD的基本概念,其中領域物件以及貧血物件的概念相當重要,雖然文中評價貧血物件不符合OOP設計需求,但事實上貧血物件會廣泛流行,其中最重要的就是有快速開發小專案以及程式處理上較為簡單的優勢。但假使遇到需要較為複雜設計的大型系統,貧血的設計又會顯得綁手綁腳,因此如何權衡就是相當重要的課題。而怎樣規模的專案能稱之為大專案,要多複雜才能叫做複雜系統?這類問題其實都沒有標準答案,或許這就需要考驗程式設計師的設計功力了。 ***产品代码都给你看了,可别再说不会DDD*** (https://docs.mryqr.com/ddd-introduction/)原文內有提供案例,若有興趣可以自行延伸閱讀。 [下一章](https://hackmd.io/N7N979nBR6G06d3BnTxN3g)
×
Sign in
Email
Password
Forgot password
or
By clicking below, you agree to our
terms of service
.
Sign in via Facebook
Sign in via Twitter
Sign in via GitHub
Sign in via Dropbox
Sign in with Wallet
Wallet (
)
Connect another wallet
New to HackMD?
Sign up