聊一聊DDD中的值對象


    在DDD的戰術設計中,值對象相對來說是一個比較簡單的概念,相對於實體、聚合根、事件處理等戰術工具來說,簡單很多。但是使用好值對象卻可以帶來非常大的好處,對代碼的可讀性,內聚性,可測試性等方面都有很大幫助,個人覺得在DDD體系里值對象工具是一個學習投入產出比很高的工具。 這篇文章我們就來聊一聊值對象,以及引申出的Domain Primitive概念。

    在查看ddd-sample-code(https://github.com/citerus/dddsample-core)時,會發現類似如下形式的一些值對象

這些值對象看起來什么也沒做,只是簡單地封裝了一個String,乍看起來有點過度設計之嫌。經過一番研究之后,發現這種寫法還是很有好處的,我們以用戶注冊領域中的手機號字段為例,來分析下使用簡單String類型和使用值對象包裝類型的區別。對於User類中的手機號字段,大部分人會用一個String phoneNumber 來表示,而如果模仿ddd-sample-code中的寫法,手機號會封裝成一個值對象,類似下面這樣:

public class PhoneNumber { private final String number; public String getNumber() { return number; } public PhoneNumber(String number) { if (number == null) { throw new ValidationException("number不能為空"); } else if (isValid(number)) { throw new ValidationException("number格式錯誤"); } this.number = number; } public String getAreaCode() { for (int i = 0; i < number.length(); i++) { String prefix = number.substring(0, i); if (isAreaCode(prefix)) { return prefix; } } return null; } public static boolean isValid(String number) { String pattern = "^0?[1-9]{2,3}-?\\d{8}$"; return number.matches(pattern); } } 

 

我們來想想將一個phone字段封裝成一個PhoneNumber類后,我們獲得了什么:

  1. 一個類。我們獲得了一個PhoneNumber Class,在這個Class中我們可以收斂部分代碼,比如自身的校驗,獲取區號等方法。關於手機號處理相關的代碼都收斂到了這個Class中。
  2. 一種類型。 我們獲得了一個PhoneNumber類型,代碼庫里多了一種類型,代碼之間的交互不再是String類型,而是PhoneNumber類型了。PhoneNumber的概念突顯出來了。

 

這樣做之后可以給代碼帶來如下好處

  1. 接口更加清晰,代碼可讀性更高。有了PhoneNumber類型之后,我們接口的定義不再是String phone,而是PhoneNumber phone。 比如發送短信接口的兩種定義方式

    sendSms(String phone,String text)
    sendSms(PhoneNumber phoneNumber,String text)

    第一種形式我只能通過參數名才知道需要先傳手機號再傳信息,粗心的用戶調用時可能會犯參數錯位的錯誤,比如sendSms("myText","15088683360")這樣的調用,代碼編譯是完全合法的。第二種方式接口通過類型強制了調用方先傳手機號,語意更明確,也杜絕了調用參數錯位的問題。

  2. 校驗收斂。使用String 類型,則從 interfaces -> application -> domain 每一層接口中的手機號參數都需要做校驗,因為String類型的手機號是不可信的。使用PhoneNumber值對象作為參數后,校驗邏輯只需要在最外層做,interfaces層傳遞的String 轉成 PhoneNumber之后,后續 application、domain、infras層的接口入參都是 PhoneNumber,PhoneNumber類型只要創建出來就一定是合法可信的,不需要在做校驗(當然判空還是需要的)。

  3. 測試收斂。是由第2點帶來的額外效果。如果每一層的接口定義的參數都是 String phone類型,則每一層的接口都需要測試不合法手機號的case。手機號校驗收斂到PhoneNumber類之后,只需要針對PhoneNumber類做測試即可。

  4. 可以讓業務代碼和Entity類從細節中脫身。也是得益於PhoneNumber類收斂了和手機號相關的邏輯,業務代碼和Entity類不用再處理類似getAreaCode()之類的邏輯,這部分邏輯內聚到了PhoneNumber類中。

 

當然,除了單個字段可以wrapper成一個值對象,更多情況下是多個緊密相關的字段組成一個值對象。Dan Bergh Johnsson在他的書籍《Security by Design》中,把這些值對象稱為 Domain Primitive, 直譯過來就是領域原語。類似String、Integer等是Common Language Primitive(通用語言原語),相應地,UserId,PhoneNumber,Address等是DomainPrimitive,是用戶賬號領域的原語,他們和賬號領域強相關。經過對領域知識的不斷消化和理解,可以沉淀出一套DomainPrimitive類,他們就是這個領域的最小構造塊,是針對這個領域的一套API庫,新需求來了基於這套API庫編寫代碼,可以更快更安全。可以類比一下DSL,對於特定的領域,設計良好的DSL可以使編程大大簡化,相應地,定義領域的DomainPrimitive就像設計一套領域的DSL一樣,針對領域沉淀出一套合適的DomainPrimitive類可以使針對該領域編碼工作大大簡化。

最后再說一說哪些字段適合做成DomainPrimitive類:

  1. 有格式要求的string,有范圍要求的數值類型,有參數格式校驗的字段。
  2. 可以關聯一些行為的字段或字段集,比如手機號等。

  3. 領域中的核心概念。比如用戶注冊領域中的Phone,IdCard等,特意將這些概念建模成一個值對象,是為了概念的顯性化,可以使代碼之間的交互更加清晰。

 

研究到最后會發現,其實值對象和DomainPrimitive也只不過是OOA/OOP的基本要求,本質就是封裝、內聚、數據和行為一起放到值對象中。而我們平時編碼的時候,可能更關注業務邏輯的實現,直接用語言的基本類型+過程式的邏輯來完成大部分業務需求,沒有認真去思考一下應該怎么設計一些對象出來,怎么把領域中的一些核心概念識別出來,顯性化的表達出來,說白了是缺少OOA/OOP的一些基本素養,最終導致了面條式的代碼。 通過有意識的構建一些值對象或領域原語,我們可以積累出這個領域對應的一套API,如果設計的合理,這一套領域API是非常有價值的,有助於領域知識的提煉,傳承和表達,也有助於編寫出更優雅的代碼。

 

參考:

1. https://www.infoq.com/presentations/Value-Objects-Dan-Bergh-Johnsson/ 《Power User Of Value Object》 強烈推薦。
2.《Secure By Design》 第5章


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM