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

這個支付聚合根派生自訂單實體關(guān)聯(lián)了用戶實體,有支付行為。
客戶可以直接使用該對象的支付方法。那么經(jīng)驗豐富的讀者可能會想示例太簡單了,業(yè)務(wù)場景復(fù)雜的情況會關(guān)聯(lián)很多的實體,并且還有很多行為。聚合根的組合實體都是委托資源庫去查詢的,聚合根的創(chuàng)建意味著依賴的實體要全部加載。
這樣的多行為、多實體的會造成冗余的查詢,并且邊導(dǎo)致實體的界難以界定。后續(xù)章節(jié)CQRS會單獨講解如何設(shè)計小聚合,又回到了我們開篇所強(qiáng)調(diào)的分而治之。
package aggregate
import (
"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 //關(guān)聯(lián)用戶實體
userRepo dependency.UserRepo //依賴倒置資用戶資源庫
orderRepo dependency.OrderRepo //依賴倒置資訂單資源庫
tx transaction.Transaction //依賴倒置事務(wù)基礎(chǔ)設(shè)施
}
// 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("余額不足")
}
//扣除用戶金錢
//修復(fù)支付狀態(tài)
cmd.userEntity.AddMoney(-cmd.TotalPrice)
cmd.Order.Pay()
//委托事務(wù)基礎(chǔ)設(shè)施
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
}
工廠
實體和聚合通常會很大很復(fù)雜,尤其是聚合根。實際上通過構(gòu)造器努力構(gòu)建一個復(fù)雜的聚合也與領(lǐng)域本身通常做的事情相沖突。
在領(lǐng)域中,某些事物通常是由別的事物創(chuàng)建的,在聚合根內(nèi)部組合的實體有可能是依賴于另一些實體或條件所組成的。篇幅所限筆者不能拿太復(fù)雜的場景代碼。
當(dāng)一個客戶程序想創(chuàng)建另一個對象時,它會調(diào)用它的構(gòu)造函數(shù),可能傳遞某些參數(shù)。但是當(dāng)構(gòu)建對象是一個很費(fèi)力的過程時(對象創(chuàng)建涉及了好多的知識,包括:關(guān)于對象內(nèi)部結(jié)構(gòu)的,關(guān)于所含對象之間的關(guān)系的以及應(yīng)用其上的規(guī)則等),這意味著對象的每個客戶程序?qū)⒊钟嘘P(guān)于對象構(gòu)建的專有知識。這破壞了領(lǐng)域?qū)ο蠛途?合的封裝。如果客戶程序?qū)儆趹?yīng)用層,領(lǐng)域?qū)拥囊徊糠謱⒈灰频搅?外邊,擾亂整個設(shè)計。
一個對象的創(chuàng)建可能是它自身的主要操作,但是復(fù)雜的組裝操作不 應(yīng)該成為被創(chuàng)建對象的職責(zé)。組合這樣的職責(zé)會產(chǎn)生笨拙的設(shè)計, 也很難讓人理解。
因此,有必要引入一個新的概念,這個概念可以幫助封裝復(fù)雜的對 象創(chuàng)建過程,它就是 工廠(Factory)。工廠用來封裝對象創(chuàng)建所必 需的知識,它們對創(chuàng)建聚合特別有用。當(dāng)聚合的根建立時,所有聚 合包含的對象將隨之建立,所有的不變量得到了強(qiáng)化。
package aggregate
import (
"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 {
//綁定創(chuàng)建工廠函數(shù)到框架,
//框架會根據(jù)客戶的使用做依賴倒置和依賴注入的處理
return &OrderFactory{}
})
})
}
// OrderFactory 訂單聚合根工廠
type OrderFactory struct {
UserRepo dependency.UserRepo //依賴倒置用戶資源庫
OrderRepo dependency.OrderRepo //依賴倒置訂單資源庫
TX transaction.Transaction //依賴倒置事務(wù)組件
Worker freedom.Worker //運(yùn)行時,一個請求綁定一個運(yùn)行時
}
// 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 {
return nil, err
}
userEntity, err := factory.UserRepo.Get(userId)
if err != nil {
return nil, err
}
cmd := &OrderPayCmd{
Order: *orderEntity,
userEntity: userEntity,
userRepo: factory.UserRepo,
orderRepo: factory.OrderRepo,
tx: factory.TX,
}
return cmd, nil
}
抽象工廠
既然我們有了工廠了,更深層的解耦,何不用抽象工廠呢?
購買普通商品和購物車?yán)锏纳唐凡欢际窍聠螁??可惜普通商品不用關(guān)聯(lián)購物車,那我們又不能設(shè)計一個大聚合根。這時候就適合用抽象工廠了
先來定義購買的接口,客戶通過工廠傳入?yún)?shù)和類型,工廠返回一個抽象接口,那么客戶就可以直接調(diào)用Shop了.
package aggregate
const (
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 int
goodsId int
goodsNum int
}
func (st *shopType) GetType() int {
return st.stype
}
func (st *shopType) GetDirectGoods() (int, int) {
return st.goodsId, st.goodsNum
}
在實現(xiàn)個抽象工廠,當(dāng)然我們還要實現(xiàn)2個聚合根,它們都實現(xiàn)了Shop 方法(篇幅有限略過)。
package aggregate
import (
"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) {
// 綁定創(chuàng)建工廠函數(shù)到框架,
// 框架會根據(jù)客戶的使用做依賴倒置和依賴注入的處理。
initiator.BindFactory(func() *ShopFactory {
// 創(chuàng)建shopFactory
return &ShopFactory{}
})
})
}
// ShopFactory 購買聚合根抽象工廠
type ShopFactory struct {
UserRepo dependency.UserRepo //依賴倒置用戶資源庫
CartRepo dependency.CartRepo //依賴倒置購物車資源庫
GoodsRepo dependency.GoodsRepo //依賴倒置商品資源庫
OrderRepo dependency.OrderRepo //依賴倒置訂單資源庫
TX transaction.Transaction //依賴倒置事務(wù)組件
}
// 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領(lǐng)域模型-開篇
- golang領(lǐng)域模型-六邊形架構(gòu)
- golang領(lǐng)域模型-實體
- golang領(lǐng)域模型-資源庫
- golang領(lǐng)域模型-依賴倒置
- golang領(lǐng)域模型-聚合根
- golang領(lǐng)域模型-CQRS
- golang領(lǐng)域模型-領(lǐng)域事件
項目代碼 https://github.com/8treenet/freedom/tree/master/example/fshop
PS:關(guān)注公眾號《從菜鳥到大佬》,發(fā)送消息“加群”或“領(lǐng)域模型”,加入DDD交流群,一起切磋DDD與代碼的藝術(shù)!