Golang领域模型-聚合根

前言:聚合是要把实体、值对象等聚合起来完成完整的业务逻辑的一个存在。聚合根据上下文边界与业务单一职责、高内聚等原则,定义聚合内部应该包含哪些实体与值对象,这也是微服务为什么要用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 {
        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
}

抽象工厂

既然我们有了工厂了,更深层的解耦,何不用抽象工厂呢?
购买普通商品和购物车里的商品不都是下单吗?可惜普通商品不用关联购物车,那我们又不能设计一个大聚合根。这时候就适合用抽象工厂了

先来定义购买的接口,客户通过工厂传入参数和类型,工厂返回一个抽象接口,那么客户就可以直接调用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 {
            // 创建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

PS:关注公众号《从菜鸟到大佬》,发送消息“加群”或“领域模型”,加入DDD交流群,一起切磋DDD与代码的艺术!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,440评论 5 467
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,814评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,427评论 0 330
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,710评论 1 270
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,625评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,014评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,511评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,162评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,311评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,262评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,278评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,989评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,583评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,664评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,904评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,274评论 2 345
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,856评论 2 339