為 Gopher 打造 DDD 系列:領域模型-聚合根
前言:聚合是要把實體、值對象等聚合起來完成完整的業(yè)務邏輯的一個存在。聚合根據(jù)上下文邊界與業(yè)務單一職責、高內(nèi)聚等原則,定義聚合內(nèi)部應該包含哪些實體與值對象,這也是微服務為什么要用DDD的思想去劃分的重要原因之一:天然的高內(nèi)聚,低耦合。
Aggregate
要將實體、值對象、其他聚合在一致性邊界之內(nèi)的組合成聚合(Aggregate), 咋看起來是一件輕松的任務,但在DDD眾多的戰(zhàn)術設計中該模式是最不容易理解的。
聚合是針對數(shù)據(jù)變化可以考慮成一個單元的一組相關的對象。聚合使用邊界將內(nèi)部和外部的對象區(qū)分開來。每個聚合有一個根,這個根是一個實體,并且它是外部可以訪問的唯一的對象。根可以保持對任意聚合對象的引用,并且其他的對象可以持有任意其他的對象,但一個外部對象只能持有根對象的引用。如果邊界內(nèi)有其他的實體,那些實體的標識符是本地化的,只在聚合內(nèi)才有意義。
聚合、聚合根與戰(zhàn)術設計
為什么準確的叫聚合根而不是聚合,如果聚合不是派生于實體,這個聚合對象就形成了一個沒有邊界的對象組合。如果沒有邊界隨意的組合對象怎么還能叫戰(zhàn)術設計?戰(zhàn)術設計一定是基于模型的邊界。聚合一定是派生自實體的,所以叫聚合根,并且使用了其他的實體、值對象,當然也可以使用其他的聚合根。這樣設計的好處是可以通過根實體來做邊界的選擇組合。通常聚合根內(nèi)是強一致的事務處理,多聚合之間是最終一致的事務處理。

這個支付聚合根派生自訂單實體關聯(lián)了用戶實體,有支付行為。
客戶可以直接使用該對象的支付方法。那么經(jīng)驗豐富的讀者可能會想示例太簡單了,業(yè)務場景復雜的情況會關聯(lián)很多的實體,并且還有很多行為。聚合根的組合實體都是委托資源庫去查詢的,聚合根的創(chuàng)建意味著依賴的實體要全部加載。
這樣的有多行為、多實體的聚合會導致冗余的查詢,并且會導致聚合的邊界難以界定。后續(xù)章節(jié)CQRS會單獨講解如何設計小聚合,又回到了我們開篇所強調(diào)的分而治之。
package aggregateimport ("errors""github.com/8treenet/freedom/example/fshop/domain/dependency""github.com/8treenet/freedom/example/fshop/domain/dto""github.com/8treenet/freedom/example/fshop/domain/entity""github.com/8treenet/freedom/infra/transaction")// 支付訂單聚合根type OrderPayCmd struct {entity.Order //派生訂單實體userEntity *entity.User //關聯(lián)用戶實體userRepo dependency.UserRepo //依賴倒置資用戶資源庫orderRepo dependency.OrderRepo //依賴倒置資訂單資源庫tx transaction.Transaction //依賴倒置事務基礎設施}// Pay 支付.func (cmd *OrderPayCmd) Pay() error {if cmd.Status != entity.OrderStatusNonPayment {//不是支付狀態(tài)return errors.New("未知錯誤")}if cmd.userEntity.Money < cmd.TotalPrice {return errors.New("余額不足")}//扣除用戶金錢//修復支付狀態(tài)cmd.userEntity.AddMoney(-cmd.TotalPrice)cmd.Order.Pay()//委托事務基礎設施e := cmd.tx.Execute(func() error {if e := cmd.orderRepo.Save(&cmd.Order); e != nil {return e}return cmd.userRepo.Save(cmd.userEntity)})return e}
工廠
實體和聚合通常會很大很復雜,尤其是聚合根。實際上通過構造器努力構建一個復雜的聚合也與領域本身通常做的事情相沖突。
在領域中,某些事物通常是由別的事物創(chuàng)建的,在聚合根內(nèi)部組合的實體有可能是依賴于另一些實體或條件所組成的。篇幅所限筆者不能拿太復雜的場景代碼。
當一個客戶程序想創(chuàng)建另一個對象時,它會調(diào)用它的構造函數(shù),可能傳遞某些參數(shù)。但是當構建對象是一個很費力的過程時(對象創(chuàng)建涉及了好多的知識,包括:關于對象內(nèi)部結構的,關于所含對象之間的關系的以及應用其上的規(guī)則等),這意味著對象的每個客戶程序將持有關于對象構建的專有知識。這破壞了領域對象和聚 合的封裝。如果客戶程序屬于應用層,領域層的一部分將被移到了 外邊,擾亂整個設計。
一個對象的創(chuàng)建可能是它自身的主要操作,但是復雜的組裝操作不 應該成為被創(chuàng)建對象的職責。組合這樣的職責會產(chǎn)生笨拙的設計, 也很難讓人理解。
因此,有必要引入一個新的概念,這個概念可以幫助封裝復雜的對 象創(chuàng)建過程,它就是 工廠(Factory)。工廠用來封裝對象創(chuàng)建所必 需的知識,它們對創(chuàng)建聚合特別有用。當聚合的根建立時,所有聚 合包含的對象將隨之建立,所有的不變量得到了強化。
package aggregateimport ("github.com/8treenet/freedom""github.com/8treenet/freedom/example/fshop/domain/dependency""github.com/8treenet/freedom/infra/transaction")func init() {freedom.Prepare(func(initiator freedom.Initiator) {initiator.BindFactory(func() *OrderFactory {//注冊訂單聚合根工廠return &OrderFactory{}})})}// OrderFactory 訂單聚合根工廠type OrderFactory struct {UserRepo dependency.UserRepo //依賴倒置用戶資源庫OrderRepo dependency.OrderRepo //依賴倒置訂單資源庫TX transaction.Transaction //依賴倒置事務組件Worker freedom.Worker //運行時,一個請求綁定一個運行時}// NewOrderPayCmd 創(chuàng)建訂單支付聚合根func (factory *OrderFactory) NewOrderPayCmd(orderNo string, userId int) (*OrderPayCmd, error) {factory.Worker.Logger().Info("創(chuàng)建訂單支付聚合根")orderEntity, err := factory.OrderRepo.Find(orderNo, userId)if err != nil {returnnil, err}userEntity, err := factory.UserRepo.Get(userId)if err != nil {returnnil, err}cmd := &OrderPayCmd{Order: *orderEntity,userEntity: userEntity,userRepo: factory.UserRepo,orderRepo: factory.OrderRepo,tx: factory.TX,}return cmd, nil}
抽象工廠
既然我們有了工廠了,更深層的解耦,何不用抽象工廠呢?購買普通商品和購物車里的商品不都是下單嗎?可惜普通商品不用關聯(lián)購物車,那我們又不能設計一個大聚合根。這時候就適合用抽象工廠了
先來定義購買的接口,客戶通過工廠傳入?yún)?shù)和類型,工廠返回一個抽象接口,那么客戶就可以直接調(diào)用Shop了.
package aggregateconst (shopGoodsType = 1//直接購買類型shopCartType = 2//購物車購買類型)type ShopType interface {//返回購買的類型 單獨商品 或購物車GetType() int//如果是直接購買類型 返回商品id和數(shù)量GetDirectGoods() (int, int)}type ShopCmd interface {Shop() error}//接口的實現(xiàn)type shopType struct {stype intgoodsId intgoodsNum int}func (st *shopType) GetType() int {return st.stype}func (st *shopType) GetDirectGoods() (int, int) {return st.goodsId, st.goodsNum}
在實現(xiàn)個抽象工廠,當然我們還要實現(xiàn)2個聚合根,它們都實現(xiàn)了Shop 方法(篇幅有限略過)。
package aggregateimport ("github.com/8treenet/freedom""github.com/8treenet/freedom/example/fshop/domain/dependency""github.com/8treenet/freedom/example/fshop/domain/entity""github.com/8treenet/freedom/infra/transaction")func init() {freedom.Prepare(func(initiator freedom.Initiator) {initiator.BindFactory(func() *ShopFactory {//注冊工廠return &ShopFactory{}})})}// ShopFactory 購買聚合根抽象工廠type ShopFactory struct {UserRepo dependency.UserRepo //依賴倒置用戶資源庫CartRepo dependency.CartRepo //依賴倒置購物車資源庫GoodsRepo dependency.GoodsRepo //依賴倒置商品資源庫OrderRepo dependency.OrderRepo //依賴倒置訂單資源庫TX transaction.Transaction //依賴倒置事務組件}// NewGoodsShopType 創(chuàng)建商品購買類型func (factory *ShopFactory) NewGoodsShopType(goodsId, goodsNum int) ShopType {return &shopType{stype: shopGoodsType,goodsId: goodsId,goodsNum: goodsNum,}}// NewCartShopType 創(chuàng)建購物車購買類型func (factory *ShopFactory) NewCartShopType() ShopType {return &shopType{stype: shopCartType,}}// NewShopCmd 創(chuàng)建抽象聚合根func (factory *ShopFactory) NewShopCmd(userId int, stype ShopType) (ShopCmd, error) {if stype.GetType() == 2 {return factory.newCartShopCmd(userId)}goodsId, goodsNum := stype.GetDirectGoods()return factory.newGoodsShopCmd(userId, goodsId, goodsNum)}// newGoodsShopCmd 創(chuàng)建購買商品聚合根func (factory *ShopFactory) newGoodsShopCmd(userId, goodsId, goodsNum int) (*GoodsShopCmd, error) {}// newCartShopCmd 創(chuàng)建購買聚合根func (factory *ShopFactory) newCartShopCmd(userId int) (*CartShopCmd, error) {
再來看看客戶的使用
package domain// Shop 普通商品購買func (g *Goods) Shop(goodsId, goodsNum, userId int) (e error) {//使用抽象工廠 創(chuàng)建普通商品購買類型shopType := g.ShopFactory.NewGoodsShopType(goodsId, goodsNum)//使用抽象工廠 創(chuàng)建抽象聚合根cmd, e := g.ShopFactory.NewShopCmd(userId, shopType)if e != nil {return}return cmd.Shop()}package domain// Shop 購物車批量購買func (c *Cart) Shop(userId int) (e error) {//使用抽象工廠 購物車批量購買類型shopType := c.ShopFactory.NewCartShopType()//使用抽象工廠 創(chuàng)建抽象聚合根cmd, e := c.ShopFactory.NewShopCmd(userId, shopType)if e != nil {return}return cmd.Shop()}
目錄
golang領域模型-開篇 golang領域模型-六邊形架構 golang領域模型-實體 golang領域模型-資源庫 golang領域模型-依賴倒置 golang領域模型-聚合根 golang領域模型-CQRS golang領域模型-領域事件
項目代碼 https://github.com/8treenet/freedom/tree/master/example/fshop
推薦閱讀
站長 polarisxu
自己的原創(chuàng)文章
不限于 Go 技術
職場和創(chuàng)業(yè)經(jīng)驗
Go語言中文網(wǎng)
每天為你
分享 Go 知識
Go愛好者值得關注
