美文网首页Golang 领域模型
Golang领域模型-聚合根

Golang领域模型-聚合根

作者: 奔奔奔跑 | 来源:发表于2020-08-10 21:03 被阅读0次

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

相关文章

  • Golang领域模型-聚合根

    前言:聚合是要把实体、值对象等聚合起来完成完整的业务逻辑的一个存在。聚合根据上下文边界与业务单一职责、高内聚等原则...

  • 领域模型:聚合、聚合根

    聚合 聚合Aggregate就是一组相关对象的集合,我们把它作为数据修改和访问的单元。一个聚合包含聚合根、实体和值...

  • 微服务架构设计模式(五)业务逻辑设计

    微服务架构中的业务逻辑设计 1、使用聚合设计领域模型 1.1 为什么要使用聚合 聚合拥有明确的边界 聚合将领域分为...

  • DDD 领域驱动设计学习(六)- 聚合和资源库

    聚合的定义 聚合(以及聚合根):聚合表示一组领域对象(包括实体和值对象),用来表述一个完整的领域概念。而每个聚合都...

  • Golang领域模型-领域事件

    前言: 在DDD中,一个业务用例对应一个事务,一个事务对应一个聚合根,在一次事务中,只能对一个聚合根进行操作。那么...

  • 领域驱动设计-聚合和聚合根

    聚合 领域模型内的实体和值对象就好比个体,而能让实体和值对象协同工作的组织就是聚合,它用来确保这些领域对象在实现共...

  • Golang领域模型-开篇

    前言:八叉树是一位拥有数十年编程经验,醉心于代码艺术的工程师。freedom是他结合《实现领域驱动设计》与《六边形...

  • Golang领域模型-实体

    前言: 实体具有业务属性、业务逻辑和业务行为,是是实实在在的业务对象。在事件风暴中,我们可以根据命令、操作与事件将...

  • 设计能力 - 你如何划分领域边界

    学习完整课程请移步 互联网 Java 全栈工程师 【领域驱动设计】浅谈聚合的划分与设计 聚合以及聚合根是领域驱动设...

  • 领域驱动设计案例-图书馆图书管理

    领域模型 实体与聚合根 读者和图书是实体;由于每个读者都将有自己的借书信息(比如,什么时候借的哪本书,是否已经归还...

网友评论

    本文标题:Golang领域模型-聚合根

    本文链接:https://www.haomeiwen.com/subject/dsacdktx.html