Golang領(lǐng)域模型-聚合根

前言:聚合是要把實體、值對象等聚合起來完成完整的業(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ù)!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容