为 Gopher 打造 DDD 系列:领域模型-聚合根
前言:聚合是要把实体、值对象等聚合起来完成完整的业务逻辑的一个存在。聚合根据上下文边界与业务单一职责、高内聚等原则,定义聚合内部应该包含哪些实体与值对象,这也是微服务为什么要用DDD的思想去划分的重要原因之一:天然的高内聚,低耦合。
Aggregate
要将实体、值对象、其他聚合在一致性边界之内的组合成聚合(Aggregate
), 咋看起来是一件轻松的任务,但在DDD
众多的战术设计中该模式是最不容易理解的。
聚合是针对数据变化可以考虑成一个单元的一组相关的对象。聚合使用边界将内部和外部的对象区分开来。每个聚合有一个根,这个根是一个实体,并且它是外部可以访问的唯一的对象。根可以保持对任意聚合对象的引用,并且其他的对象可以持有任意其他的对象,但一个外部对象只能持有根对象的引用。如果边界内有其他的实体,那些实体的标识符是本地化的,只在聚合内才有意义。
聚合、聚合根与战术设计
为什么准确的叫聚合根而不是聚合,如果聚合不是派生于实体,这个聚合对象就形成了一个没有边界的对象组合。如果没有边界随意的组合对象怎么还能叫战术设计?战术设计一定是基于模型的边界。聚合一定是派生自实体的,所以叫聚合根,并且使用了其他的实体、值对象,当然也可以使用其他的聚合根。这样设计的好处是可以通过根实体来做边界的选择组合。通常聚合根内是强一致的事务处理,多聚合之间是最终一致的事务处理。
这个支付聚合根派生自订单实体关联了用户实体,有支付行为。
客户可以直接使用该对象的支付方法。那么经验丰富的读者可能会想示例太简单了,业务场景复杂的情况会关联很多的实体,并且还有很多行为。聚合根的组合实体都是委托资源库去查询的,聚合根的创建意味着依赖的实体要全部加载。
这样的有多行为、多实体的聚合会导致冗余的查询,并且会导致聚合的边界难以界定。后续章节CQRS
会单独讲解如何设计小聚合,又回到了我们开篇所强调的分而治之。
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 //关联用户实体
userRepo dependency.UserRepo //依赖倒置资用户资源库
orderRepo dependency.OrderRepo //依赖倒置资订单资源库
tx transaction.Transaction //依赖倒置事务基础设施
}
// Pay 支付.
func (cmd *OrderPayCmd) Pay() error {
if cmd.Status != entity.OrderStatusNonPayment {
//不是支付状态
return errors.New("未知错误")
}
if cmd.userEntity.Money < cmd.TotalPrice {
return errors.New("余额不足")
}
//扣除用户金钱
//修复支付状态
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
}
工厂
实体和聚合通常会很大很复杂,尤其是聚合根。实际上通过构造器努力构建一个复杂的聚合也与领域本身通常做的事情相冲突。
在领域中,某些事物通常是由别的事物创建的,在聚合根内部组合的实体有可能是依赖于另一些实体或条件所组成的。篇幅所限笔者不能拿太复杂的场景代码。
当一个客户程序想创建另一个对象时,它会调用它的构造函数,可能传递某些参数。但是当构建对象是一个很费力的过程时(对象创建涉及了好多的知识,包括:关于对象内部结构的,关于所含对象之间的关系的以及应用其上的规则等),这意味着对象的每个客户程序将持有关于对象构建的专有知识。这破坏了领域对象和聚 合的封装。如果客户程序属于应用层,领域层的一部分将被移到了 外边,扰乱整个设计。
一个对象的创建可能是它自身的主要操作,但是复杂的组装操作不 应该成为被创建对象的职责。组合这样的职责会产生笨拙的设计, 也很难让人理解。
因此,有必要引入一个新的概念,这个概念可以帮助封装复杂的对 象创建过程,它就是 工厂(Factory)。工厂用来封装对象创建所必 需的知识,它们对创建聚合特别有用。当聚合的根建立时,所有聚 合包含的对象将随之建立,所有的不变量得到了强化。
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 {
//注册订单聚合根工厂
return &OrderFactory{}
})
})
}
// OrderFactory 订单聚合根工厂
type OrderFactory struct {
UserRepo dependency.UserRepo //依赖倒置用户资源库
OrderRepo dependency.OrderRepo //依赖倒置订单资源库
TX transaction.Transaction //依赖倒置事务组件
Worker freedom.Worker //运行时,一个请求绑定一个运行时
}
// NewOrderPayCmd 创建订单支付聚合根
func (factory *OrderFactory) NewOrderPayCmd(orderNo string, userId int) (*OrderPayCmd, error) {
factory.Worker.Logger().Info("创建订单支付聚合根")
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
}
抽象工厂
既然我们有了工厂了,更深层的解耦,何不用抽象工厂呢?购买普通商品和购物车里的商品不都是下单吗?可惜普通商品不用关联购物车,那我们又不能设计一个大聚合根。这时候就适合用抽象工厂了
先来定义购买的接口,客户通过工厂传入参数和类型,工厂返回一个抽象接口,那么客户就可以直接调用Shop了.
package aggregate
const (
shopGoodsType = 1//直接购买类型
shopCartType = 2//购物车购买类型
)
type ShopType interface {
//返回购买的类型 单独商品 或购物车
GetType() int
//如果是直接购买类型 返回商品id和数量
GetDirectGoods() (int, int)
}
type ShopCmd interface {
Shop() error
}
//接口的实现
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
}
在实现个抽象工厂,当然我们还要实现2个聚合根,它们都实现了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) {
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 创建商品购买类型
func (factory *ShopFactory) NewGoodsShopType(goodsId, goodsNum int) ShopType {
return &shopType{
stype: shopGoodsType,
goodsId: goodsId,
goodsNum: goodsNum,
}
}
// NewCartShopType 创建购物车购买类型
func (factory *ShopFactory) NewCartShopType() ShopType {
return &shopType{
stype: shopCartType,
}
}
// NewShopCmd 创建抽象聚合根
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 创建购买商品聚合根
func (factory *ShopFactory) newGoodsShopCmd(userId, goodsId, goodsNum int) (*GoodsShopCmd, error) {}
// newCartShopCmd 创建购买聚合根
func (factory *ShopFactory) newCartShopCmd(userId int) (*CartShopCmd, error) {
再来看看客户的使用
package domain
// Shop 普通商品购买
func (g *Goods) Shop(goodsId, goodsNum, userId int) (e error) {
//使用抽象工厂 创建普通商品购买类型
shopType := g.ShopFactory.NewGoodsShopType(goodsId, goodsNum)
//使用抽象工厂 创建抽象聚合根
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()
//使用抽象工厂 创建抽象聚合根
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
自己的原创文章
不限于 Go 技术
职场和创业经验
Go语言中文网
每天为你
分享 Go 知识
Go爱好者值得关注