使用Typescript實現DDD領域建模 - Matthew de Nobrega
Typescript提供了一系列用於構建富域模型的工具。然而,有很多方法可以解決這個問題,並且需要解決一些棘手的挑戰。
任何方法必須解決的主要挑戰是:
- 序列化/反序列化:來自永續性和傳輸層的資料是無型別的,需要進入“型別安全區域”
- 處理聚合,值物件和列表
- 支援聯合型別,多型和繼承
- 不可變性 - 雖然這不是一項要求,但優點是足以支援建立不可變模型的方法
介面
開始需要序列化的資料建模域的第一步是:為資料定義介面,並將原始資料轉換為這些介面:
- 定義這些介面可以依賴工具的幫助,因為工具有更多的資訊,編譯器可以捕獲明顯的錯誤,如嘗試訪問不存在的欄位。
- 然而,這種方法不支援不變性並且不提供型別安全性 - 不正確的資料將被傳入而沒有錯誤,會在型別系統中被認為是物件的樣子與其真實樣子之間產生未識別的不匹配。只有當程式碼嘗試執行訪問不存在的屬性之類的操作時,才會識別出這種情況。
- 轉換為介面不會向領域物件新增任何功能,因此根據定義它們是貧血的。
interface IPerson { name:string // name理論上不能未定義 } const person = <Person> deserializedData console.log(person.name)//不保證名稱已定義 |
建設者
一種常用的方法是使用構建器Builder模式:
- 構建器可以進行現場級驗證,並可以在返回之前檢查完整物件,因此可以保證型別安全。
- 構建器將構造物件與輸入的引數分離,這具有優點,但是為非編碼資料的編組增加了大量開銷。
- 構建器可以用於生成不可變的域物件,但是具有改變這些物件的狀態的域方法變得非常麻煩,並且使用類引用構建器是醜陋的。
- 構建器增加了很多開銷 - 每個域類都需要定義一個構建器,並且兩者需要保持同步
class Person { readonly name: string addLastName(lastName: string): Person { return new PersonBuilder() .setName(`${this.name} ${lastName}`) .getResult() } } class PersonBuilder { getResult(): Person {} // Throw if name not initialized setName(name: string): PersonBuilder {} // Throw for invalid name } const person = new PersonBuilder() .setName(deserializedData.name) .getResult() |
使用建構函式
雖然有很多關於建構函式應該具有多少複雜性的討論,但是看到建構函式的唯一作用是建立非型別化資料的型別安全例項,並且對該資料進行可選更新,從而導致可讀,型別安全碼:
// Get the field value from the update, falls back to the item function fallback(update?: any, item: any, field: string) {} class Person { readonly name: string constructor(item: any, update?: any) { if (!item) { throw new Error('Item not supplied') } this.name = fallback(update, item, 'name') if (!this.name) { throw new Error('Name not supplied') } } addLastName(lastName: string): Person { return new Person(this, { name: `${this.name} ${lastName}` }) } } const person = new Person(deserializedData) |
- 與構建器一樣,支援欄位級和完整物件檢查,並且可以保證型別安全
- 構造複雜物件可以在一行中完成,而不是使用構建器或對映器的每個欄位的行
- 更改狀態的域模型方法可以使用建構函式的update引數在一行中執行此操作,而不是使用構建器逐行重新建立完整物件
- 一個類的所有構造和驗證邏輯都在一個地方
額外巢狀物件和聯合型別
領域驅動設計提倡使用聚合進行域建模,聚合通常具有巢狀值物件。使用構建器或對映器處理這些情況變得很麻煩,但使用建構函式是乾淨的:
class Address { readonly postalCode: string // Obvious constructor.. } class Person { readonly address: Address readonly name: string constructor(item: any, update?: any) { if (!item) { throw new Error('Item not supplied') this.address = new Address(fallback(update, item, 'address')) this.name = fallback(update, item, 'name') if (!this.name) { throw new Error('Name not supplied') } } } const person = new Person(deserializedData) |
聯合型別是模擬“同一事物的不同型別”的好方法,並且可以使用輔助方法支援:
class PostalAddress { readonly postalCode: string } class ResidentialAddress { readonly street: string } type Address = PostalAddress | ResidentialAddress // Takes raw address and returns constructed address function address(item: any): Address {} class Person { readonly address: Address readonly name: string constructor(item: any, update?: any) { if (!item) { throw new Error('Item not supplied') } this.address = address(fallback(update, item, 'address')) this.name = fallback(update, item, 'name') if (!this.name) { throw new Error('Name not supplied') } } } const person = new Person(deserializedData) |
在過去的六個月裡,我每天都在使用這種模式,這是我能找到的最簡潔的方法,用於支援不可變性並保證序列化邊界的型別安全性。
警告:
- 雖然可以在建構函式中重塑資料,但是每次要序列化時都需要“解構器”來反轉此過程。我發現保持對稱性:item = new Item(JSON.parse(JSON.stringify(item))使得程式碼更加清晰,代價是遠離'純'DDD。
- 上述方法不保證物件和陣列欄位的不變性。為此可以使用像immutable一樣的庫,但這會破壞序列化/反序列化的對稱性,對我來說,只是不編寫改變物件和陣列欄位的程式碼就更清晰了。
- 有一個論點要求物件的完全例項化使單元測試變得困難 - 即你需要提供物件的所有欄位來測試任何一個方法。我正在解決這個問題,透過定義一個有效的存根物件來執行單元測試 - 可以使用建構函式的update引數設定存根物件狀態的任何必需變體:const newItem = new Item(stub,{name:'測試'})
相關文章
- DDD學習(二)—— 領域建模重要概念
- DDD+Javascript領域建模示例 -Alex LawrenceJavaScript
- 使用使用者故事對映實現領域建模 - pulse
- 非同步架構思維:使用Akka實現領域建模非同步架構
- 請大家推薦一款適合DDD領域建模的建模工具!
- 使用知識圖實現領域知識建模與測試
- 使用業務能力方法實現DDD戰略建模 - pulse
- TypeScript如何實現DDD的值物件?TypeScript物件
- DDD-領域物件與領域服務物件
- DDD領域驅動設計:領域事件事件
- DDD建模心得:領域概念建模是一種語文語法分析練習 - prefactordesign語法分析
- 根據業務能力實現DDD建模 - trond
- 運用領域模型——DDD模型
- 用 F#和EventStore實現DDD領域驅動設計
- 如何進行高質量的DDD領域建模?什麼是領域模型?如何捕捉?尺寸如何? - Manning模型
- 領域驅動模型DDD(二)——領域事件的訂閱/釋出實踐模型事件
- 基於COLA架構建立運輸微服務應用和DDD領域建模架構微服務
- 請教banq老師怎樣學習DDD領域建模和設計模式設計模式
- 在DDD中建立領域模型模型
- 領域驅動設計(DDD)實踐之路(一)
- CloudNotes之領域建模篇:領域模型簡介Cloud模型
- 財務建模最佳實踐 - DDD相關建模
- 關於java領域建模疑惑Java
- 物件導向與領域建模物件
- DDD劃分領域、子域,核心域,支撐域的目的
- 使用Spring Data JPA在更改實體時釋出DDD領域事件 - thorbenSpring事件ORB
- 如何實現DDD事件建模的詳細步驟 - goeleven事件Go
- 領域驅動模型DDD(三)——使用Saga管理事務模型
- DDD領域驅動設計初探(5):AutoMapper使用APP
- 使用領域驅動設計DDD和CQRS實現身份驗證的微服務原始碼專案微服務原始碼
- 淺談DDD(領域驅動設計)
- 淺談 DDD 領域驅動設計
- DDD-領域驅動設計示例
- ABP與DDD領域驅動關係
- 使用Akka實現Reactive DDDReact
- 架構師之路 - 業務領域建模架構
- 用形而上學進行領域建模
- 讀《實戰DDD(Domain-Driven Design領域驅動設計:Evans DDD)》想到的AI