不想只做Cruder?實體、聚合根,還不快去了解下
責編 | 韓楠
約 4895 字 | 10 分鐘閱讀
以下,Enjoy~
《DDD在Go中的落地》這一專題,每篇文章開頭,我打算都先簡單地跟你聊聊為什麼要這麼做,比如為何需要使用該領域物件,它解決了什麼問題,其次才是對應到程式碼上該如何實現。
有的時候,知道 Why 比知道 How 要重要的多,也希望你能夠做到知其然知其所以然。
過往的 一篇分享中,我曾闡述了這樣一個觀點,對於業務系統,要儘量保持程式碼的清晰,這樣才能帶來較低的維護成本。值物件基於業務域中的統一語言,將相關聯的屬性組合在一起,構成了一個完整的概念整體,從而讓業務邏輯更內聚,業務概念也得到了更好的展現。
01 ⎪ 什麼是實體
先來看看怎麼理解實體。這裡直接引用 IDDD 一書中對實體的定義:
一個實體是一個唯一的東西,並且可以在相當長的一段時間內持續地變化。我們可以對實體做多次修改,故一個實體物件可能和它先前的狀態大不相同。但是,由於它們擁有相同的身份標識(identity),它們依然是同一個實體。
—— from IDDD
02 ⎪ 區別於資料物件
實體有兩個突出的特徵:唯一的身份標識和可變性,而這兩個特徵同樣存在於資料物件身上,因此,為了避免先入為主地將資料物件等同於實體,這裡先說下兩者的區別和聯絡。
▶︎ 資料物件
資料物件一般指的是我們在 model 層定義的一些 struct,這些 struct 的屬性跟資料庫中某個表的列資訊是保持一致的,透過 ORM 軟體(我們常用的就是Gorm),可以方便地將資料庫表裡的一行對映成一個資料物件的例項。
反之亦然。
這些物件之所以叫資料物件,主要原因在於它們只是承載了資料功能。
比如,在一個名為 User 的資料模型中:
我們可以看到它包含了使用者名稱、手機號、性別、年齡,等等。但是,單從這個資料模型上是看不出它可以做什麼的。
而具體能做什麼、怎麼做,則被放到了某個服務(通常會有個 service 層)裡面,在服務中透過一些賦值操作 來更新資料物件的某些屬性,最後再透過 ORM 儲存回資料庫中。
這種模式下, 資料物件因為缺少了行為,又被稱為貧血模型。
從本質上來說,這種方式依然是程式導向的程式設計正規化,本應屬於領域模型的業務邏輯 被洩露到了各個 service 中,久而久之,會使得程式碼越來越難以理解。
▶︎ 實體物件
實體是 DDD 中的領域物件,它是一個富有行為的領域概念。
領域物件裡的成員和資料物件裡的成員可能是一致的,也可能不一致,這完全取決於你使用什麼樣的儲存技術。
比如,在 MongoDB 這類文件型資料庫中,實體模型和資料模型很可能是高度一致的,但是在傳統的 MySQL 資料庫中,很多時候會將一個實體模型對映成多個資料模型。
考慮在訂單中要有配送地址這個場景。
如果是使用 MongoDB ,可能直接存成一個doc:
而在 MySQL 中,就需要將地址資訊拆到另外一張表裡。
除此以外, 實體物件跟資料物件的本質區別,還在於模型的豐富程度,實體物件是包含了豐富的領域概念的。
還是訂單這個例子, Order 實體上可能還會定義一些領域方法:
而資料模型就只是光禿禿的一個 Order 結構體。
▶︎ 唯一標識不等同於資料庫主鍵
說到唯一標識,我們很容易聯想到資料庫表裡的唯一主鍵,認業務的唯一標識就是表記錄裡的id列,其實這種理解是不太全面的。
資料表裡的主鍵 id 在某些情況下,可以作為實體的唯一標識,但兩者本身屬於不同的概念。
還是以 Order 實體為例,它在資料庫中可能存在類似下面這樣的一張表:
那麼,這裡的 id 只是資料庫表裡的一個主鍵,而 order_id 才是 Order 這個領域實體的唯一標識。
再來看一個 Product 的例子,它的定義比較簡單:
products 表有一個 id 列作為主鍵,同時,我們通常也會將這個 id 作為 Product 實體的唯一標識。也就是說,資料庫表的主鍵,有的時候可以作為唯一標識使用,有的時候卻不可以。
總之,我們只需要記住, 唯一標識和資料庫結構沒有關係,主鍵 id 是儲存層面的唯一標識,而業務層面的唯一標識才是實體關心的。
03 ⎪ 如何表示唯一標識
比較教條的做法是無論什麼情況,都用一個值物件來存放實體的唯一標識。
值物件具有不變性,這也就保證了實體身份的穩定。
但在一些比較簡單的情況下,可以直接使用原始型別(比如string、int)來作為唯一標識。
▶︎ 使用值物件表示唯一標識
這裡考慮一個訂單號生成的例子,假如我們生成訂單號的規則如下:
時間戳+業務型別+下單平臺+隨機碼(或自增碼,自增碼每天可清零)+支付渠道
那麼,透過這樣一個訂單號,我們可以解析出該訂單下單的時間、支付的渠道等資訊。
這些資訊的解析,跟訂單號是密不可分的,這些行為和訂單號本身形成了一個完整的業務概念整體。因此,這個時候將訂單號編碼為一個值物件是合理的。
使用值物件來實現唯一標識, 不僅能夠更好地表達業務,同時,可以一定程度上規避一些錯誤。
看下面的程式碼,我們要提供一個根據訂單ID和商品ID,來獲取訂單項資訊的函式:
第一種實現的入參都是 int64 型別,第二種是值物件型別。
對於第一種來說,呼叫方即使在傳參時將 orderId 和 productId 搞反了,編譯器也是不會報錯的,而這種錯誤在第二種實現方式中,是完全不可能發生的。
這種表示方式的唯一缺點,在於程式碼量的增加。
在很多地方,因為必須要對原始型別與值物件型別進行轉換(比如資料庫裡儲存的訂單號還是 int 型別,但是讀取出來要轉成領域實體,就需要轉成 OrderId 型別,在實體持久化的時候還需要將 OrderId 轉成 int),複雜度會有一定的增加。
▶︎ 直接使用 int64 作為唯一標識
上面提到的產品ID,是一個不需要使用值物件做唯一標識的例子。
Product 實體用自增 ID 來代表唯一標識,這個 ID 除了能唯一標識一個產品外,沒有其他任何與之相關的行為。
所以這裡可以將其簡化成一個 int64 型別。int64 也是不可變的,因此其本質上也是符合值物件的特點的。
這種實現方式的缺點是表達能力不強,但好在足夠簡單。
綜合來看:
• 如果是使用通用的ID生成器這類的工具,來生成唯一標識,其本身除了唯一標識一個實體,也沒有其他的行為,這種情況下,推薦直接使用原始型別就可以了。
• 如果唯一標識,是按照一定的規則來生成的,並且圍繞這個唯一標識還會有一些方法(行為),這個時候最好使用值物件來承載。
04 ⎪ 生成唯一標識
根據不同的場景,大體上分為兩種生成方式:使用者傳入和系統生成。
▶︎ 使用者直接傳入唯一標識
這種情況依賴使用者的輸入,使用者需要保證輸入的唯一標識真的是唯一的,這通常很難。但是,在某些特殊場景下還是可以做到的。
比如在學校圖書館,管理員錄入書本的場景。
我們知道每本書都有一個 ISBN 碼,這個 ISBN 碼就可以作為書本的唯一標識。管理員在錄入書本時,用手持裝置直接掃描 ISBN 碼,這個掃描到的 ISBN 碼,就可以認為是使用者直接傳入的唯一標識。
▶︎ 系統生成唯一標識
大部分情況下,我們遵循的都是這種方法。無論是上面提到的 OrderId 還是 ProductId,雖然生成的方式不同,但都可以歸屬到系統生成的範疇。
這裡面有一種比較特殊的情況,是使用資料庫的自增來生成。
這種情況特殊的點在於,實體建立好了之後,可能還沒有來得及分配一個唯一標識,因為此時實體還沒有進行持久化。
沒有唯一標識的實體,可能需要面對下面兩個問題:
如果需要釋出領域事件,這個時候因為還沒生成唯一標識,事件接收者無法知道是哪個實體發出的事件;
說到這,你可能會問了,那怎麼解決這個問題呢?辦法是有,就是將唯一標識的生成提前, 在實體建立好的時候,保證一定是有唯一標識的。
▶︎ 程式碼如何實現
通常,我們會在 Repository 介面中,定義一個 NextIdentity 方法,如下:
注:Repository 對應的是 DDD 裡的倉儲概念,我們在後面的章節會介紹。
而需要用到這個 NextIdentity 方法的地方,一般是在工廠或應用服務裡。
注:工廠和應用服務也是 DDD 裡的概念,同樣放在後面的章節進行介紹。
對於應用服務,基本都需要持有一個對應的 Repository 屬性,比如這樣:
而如果是在工廠裡,就要展開討論了。
如果工廠是無狀態的,也可以讓工廠直接持有對應 Repository 屬性,實現方式跟在應用服務裡類似。
如果工廠是有狀態的,那麼只能每次前都建立一個工廠的例項,在建立的時候將 Repository 作為引數傳入:
無論是哪種形式,都需要先生成唯一標識,再生成實體。
05 ⎪ 聚合根
聚合,是 DDD 中較為難以理解的一個概念。
很多剛剛接觸 DDD 的同學,常犯的錯誤是設計出一個囊括天地萬物的大聚合。這在戰略設計階段往往看不出什麼問題,但是一旦要落實到程式碼層面,就會發現根本行不通。
當然,我們這裡也不會過多去講應該如何設計聚合,這不是這篇文章的重點。
但是,我們至少要知道,設計小聚合是很重要的一條原則。
小聚合的前提,是要保證聚合內的一致性條件不被破壞。這裡有篇文章可以幫助大家更好的理解,得空了可以看下。()
更多的原則,還可參考這裡:(https://weread.qq.com/web/reader/f5032ce071fd5a64f50b0f6kad63251024aad61ab143c7e)
當我們迴歸到小聚合的設計後,就會發現,聚合根的實現方式跟實體是非常類似的。
還是考慮 Product 這個例子:
假如說我們的產品模型非常簡單,只有前面的四個屬性,我們是否還有必要再單獨定義一個 ProductAggregate 結構體做聚合根呢?
上面這個寫法顯然是多餘的,Product 是實體,但是,如果在聚合根裡只包含實體一個屬性時,Product 本身也可以當作聚合根來用。
同理,可以推廣到稍複雜的情況。
比如在一個訂單中,通常會包含總價、支付方式、訂單項、地址等內容,如果我們強制區分實體和聚合根的話,可能會寫出如下的程式碼:
但在實際開發中,可以在 Order 中直接引用 OrderItem,這樣就省去了對 OrderAggregate 的維護,Order 也變成了實體加聚合根的雙重身份:
同時,我們建議所有的聚合根在命名上都加一個 Aggregate 或 Agg 的字尾,用以明確地表示這是一個聚合根。
使用聚合根時,還有一個經常容易犯的錯誤,就是在一個聚合根中引用了另外一個聚合根。
正確的做法是 透過全域性唯一標識來引用外部的聚合。
原因就在於,在一個事務中,原則上只能修改一個聚合,如果不持有對其他物件的引用,也就避免了對這個物件的修改。
在你的程式碼裡,如果一個事務必須要修改多個聚合,這個時候就要考慮聚合設計的是否合理,這種情況通常意味著聚合的一致性邊界是錯誤的。
06 ⎪ 結語
今天,我主要為你介紹了圖中包含的這些內容。實體、聚合根,是DDD領域層最核心的概念,其上可能包含了對多個值物件的引用,同時也是業務邏輯主要的載體。
整體來說,實體的定義跟普通的 Struct 並無太大的區別,唯一需要注意的就是唯一標識的表示。
現在,實體有了,接下來的工作就是如何將其持久化到資料庫中,但實體畢竟不同於資料模型,沒法直接呼叫 Gorm 的相關方法。
具體如何做呢?留待我們在下一章節-倉儲,再細說。
▶︎ 延伸思考
最後再進一步思考個問題吧。實體和值物件在DDD中是非常重要的領域載體,一般在建模的時候,推薦儘可能地使用值物件來代替實體。有的同學這個時候可能就糊塗了,一件東西要麼是實體要麼是值物件,還能同時兼顧兩者嗎?這就要分場景來看了。
我們知道實體和值物件最大的不同,在於是否具有唯一標識。而在某些情況下,我們關心的只是一件東西的屬性。
比如我們在網上購買了一臺電視,剛送過來的時候你覺得有瑕疵,於是更換了一臺,你要關心的是不是就是更換的這臺跟你購買的是否為同一型號、同一尺寸,但是對於經銷商來說,他關心的就是你退回的是不是之前發給你的那臺。
所以說,同樣是電視這個物品,在你這裡就可以看作是一個值物件,而在經銷商那裡,就必須看作是一個實體了。
好,今天就先交流到這,後續再見~
THE END
轉載請聯絡ITPUB官方公眾號獲得授權
—————————————————————————————————
歡迎各領域技術人員投稿
投稿郵箱 | hannan@it168.com
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/70016482/viewspace-2907514/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- DDD之4聚合和聚合根
- 領域設計:聚合與聚合根
- 自媒體軟體哪個好?這幾大軟體,還不快收藏?
- DDD的實體、值物件、聚合根的基類和介面:設計與實現物件
- DDD中聚合、聚合根的含義以及作用
- 反饋這麼慢?很抱歉,我不想堅持下去了
- node資料加密,還不快快點進來?加密
- DDD | 04-什麼是聚合根
- 十年過去了,還能看到拉拉人
- 剛上線的優美圖片網站,還不快來網站
- Mysql備份還有這麼多套路,還不瞭解下?MySql
- 基於ABP落地領域驅動設計-02.聚合和聚合根的最佳實踐和原則
- 震坤行API介面聚合解析,實現根據ID取商品詳情API
- es java 聚合方法——聚合後根據count排序並取前2條資料Java排序
- 想要一夜暴富嗎?還不快去GPT store上創業?GPT創業
- 什麼是聚合頁面?seo做聚合頁面的好處
- 不想找建站公司還有其他建站途徑嗎?
- 不想eject,還咋修改create-react-app的配置?ReactAPP
- 做聚合支付代理,究竟有前途嗎?
- 今世還願與你相伴,國風仙俠真實社交手遊《夢幻逍遙》今日正式全網首發,還不快撩!
- 不想敲程式碼雲端計算瞭解下?千鋒雲端計算教程限時領取
- nginx根據token做頻率限制Nginx
- 瞭解下C# 結構體(Struct)C#結構體Struct
- vue怎麼設定html不快取 但是js、css等檔案做快取VueHTML快取JSCSS
- 多個電商平臺API介面聚合解析,實現根據關鍵詞取商品列表API
- JWT 還是 session 根據實際業務使用不是更好嗎JWTSession
- 介面 做具體的實現
- 50年過去了,我們還需要《龍與地下城》嗎?
- 24天過去了,《摩爾莊園》為什麼還沒涼?
- 還在用ifelse來寫業務?瞭解下Spring狀態機Spring
- 做聚合支付收款碼有發展前景嗎?
- java 只列印實體類裡的 非 null 屬性JavaNull
- 做前端的你還沒用這些軟體?? out 啦前端
- 14 張有趣深動圖解 FlexBox,好傢伙,還不快進收藏夾吃灰!圖解Flex
- 《校花的貼身高手:天階島》首測開啟 還不快來保護我
- 年薪60萬大資料架構師教你Hadoop如何安裝!還不快來看!大資料架構Hadoop
- 網付聚合碼和普通收款碼有什麼區別?一起來了解下!
- C#根據經緯度獲取實體地址C#