DDD

DDD项目分解

DDD项目分解

Posted by SkioFox on February 20, 2024

一、DDD架构选型

无论是DDD四层架构、六边形架构、洋葱架构等都共同体现了高内聚,低耦合的设计特性。
由于DDD四层架构更容易理解与入手,所以我们选择DDD四层架构

1、DDD分层架构

DDD分层架构中有很重要的依赖原则:每层只能与位于下方的层发生耦合,类似于网络的7层或TCP/IP的4层模型架构,每一层各司其职,并且只关心向下一层的实现,而不会出现各层耦合。
DDD分层架构中包含四层:从上到下分别是用户接口层,应用层,领域层和基础层。

用户接口层

  • 微服务面向不同前端时,需要展示的数据可能不同,此时由于需要保持领域核心业务逻辑的稳定,不可能去定制开发各种领域服务和应用服务编排。因此,为避免暴露服务端业务逻辑,防止非必需的字段数据外泄 ,同时保证领域逻辑的干净
    应用层。
  • 它负责展现层与领域层之间的协调,协调业务对象来执行特定的应用程序任务。它不包含业务逻辑,主要负责编排和转发,即将实现的功能委托给一个或多个领域对象来实现,它本身只负责处理业务用例的执行顺序以及结果的拼装。通过这样一种方式,它隐藏了领域层的复杂性及其内部实现机制。

领域层

  • 领域层位于应用层之下,是领域模型的核心,主要实现领域模型的核心业务逻辑,体现领域模型的业务能力
  • 领域层关注实现领域对象的充血模型和聚合本身的原子业务逻辑,至于用户操作和业务流程,则交给应用层去编排。这样设计可以保证领域模型不容易受外部需求变化的影响,保证领域模型的稳定
  • 跨多个聚合的领域逻辑在领域层实现,由领域服务组织和协调多聚合的多实体,实现原子业务逻辑

基础层

  • 基础层贯穿了DDD所有层,包括第三方工具,API网关,消息中间件,分布式事务,消息最终一致性能力,数据库,缓存能能力的提供。
  • 基础层有仓储模式的代码逻辑,通过仓储接口和仓储实现,解耦领域层和基础层,保证领域核心业务逻辑的干净,降低DB资源变化给领域层带来的影响,这部分内容,请见下回分解。

2、DDD各层的主要职责和怎么分工协作

3、代码架构

对比正常的DDD四层架构,Server代替用户接口层的,DTO放到app中,事件发布放再vars,订阅为subscriber

4、代码实例

1) 账号A给账号B转账10块

账号是一个聚合,Account是聚合根。逻辑如下

  • ① 账户是否合法
  • ② 判断金额是否足够
  • ③ 转账逻辑
  • ④ 短信通知

按照应用服务与领域服务之间的职责,应该①②③ 应该放在领域服务中,④ 则与主线逻辑无关放在应用服务。以为是按照云行销架构展示代码

账号应用服务

// app/account.go
// 账号应用服务
type AccountService struct {
 accountDS account.AccountServiceIFace
}

func NewAccountService(accountDS account.AccountServiceIFace) *AccountService {
 return &AccountService{
  accountDS: accountDS,
 }
}

func (as *AccountService) TransferAccounts(ctx context.Context, sourceAccountID uint64, destAccountID uint64, amount float32) error {
 if err := as.accountDS.TransferAccounts(ctx, sourceAccountID, destAccountID, amount); err != nil {
  return err
 }

 // 发布事件,处理短信通知
 return vars.EventPublisher.Publish(ctx, "account", "transfer", map[string]interface{}{
  "sourceAccountID": sourceAccountID,
  "destAccountID":   destAccountID,
  "amount":          amount,
 })
}

账号领域服务

// domain/account/service.go
// 账户领域服务接口
type AccountServiceIFace interface {
   TransferAccounts(ctx context.Context, sourceAccountID uint64, destAccountID uint64, amount float32) error
}
 
// 账户领域服务
type AccountService struct {
   accountRepo AccountRepoIFace
}
 
func NewAccountService(accountRepo AccountRepoIFace) *AccountService {
   return &AccountService{
      accountRepo: accountRepo,
   }
}
 
// 转账
func (ds *AccountService) TransferAccounts(ctx context.Context, sourceAccountID uint64, destAccountID uint64, amount float32) error {
   if amount <= 0 {
      return errors.New("转账金额不能小于0")
   }
 
   // 读取账号A
   sourceAccount, err := ds.accountRepo.GetAccount(ctx, sourceAccountID)
   if err != nil {
      return err
   }
 
   // 读取账号B
   destAccount, err := ds.accountRepo.GetAccount(ctx, destAccountID)
   if err != nil {
      return err
   }
 
   // 减少账号A的金额
   if err := sourceAccount.DecreaseBalance(amount); err != nil {
      return err
   }
 
   // 增加账号B的金额
   if err := destAccount.IncreaseBalance(amount); err != nil {
      return err
   }
 
   // 开启事务
   return ds.accountRepo.Translation(ctx, func(newCtx context.Context) error {
      //  存储账号聚合A
      if err := ds.accountRepo.Update(newCtx, sourceAccount); err != nil {
         return err
      }
 
      //  存储账号聚合B
      if err := ds.accountRepo.Update(newCtx, destAccount); err != nil {
         return err
      }
 
      return nil
   })
}

账号聚合

// domain/account/account.go
// 账号聚合根
type Account struct {
   ID     uint64  `json:"id"`     // ID
   Amount float32 `json:"amount"` // 金额
}
 
func NewAccount(amount float32) (*Account, error) {
   if amount < 0 {
      return nil, errors.New("账号金额不能小于0")
   }
   return &Account{
      ID:     itool.GenerateSId(),
      Amount: amount,
   }, nil
}
 
// 扣除金额
func (a *Account) DecreaseBalance(amount float32) error {
   if amount < 0 {
      return errors.New("扣除金额必须大于0")
   }
   if amount > a.Amount {
      return errors.New("账户余额不足")
   }
   a.Amount = a.Amount - amount
   return nil
}
 
// 增加金额
func (a *Account) IncreaseBalance(amount float32) error {
   if amount < 0 {
      return errors.New("增加余额必须大于0")
   }
   a.Amount = a.Amount + amount
   return nil
}

上面是一个很简单的例子,调用应用层转账服务,应用层编排调用领域服务,领域服务调用账号聚合A扣除金额,账号聚合B的增减金额,然后将聚合落库。

2) 为账号增加一个地址值对象,了解值对象在聚合中的操作

地址值对象

// domain/account/address.go
// 地址值对象
type Address struct {
   Province string `json:"province"` // 省
   City     string `json:"city"`     // 市
   District string `json:"district"` // 区
   Address  string `json:"address"`  // 地址
}
 
func NewAddress(province, city, district, address string) (*Address, error) {
   if province == "" || city == "" || district == "" || address == "" {
      return nil, errors.New("地址不能为空")
   }
 
   return &Address{
      Province: province,
      City:     city,
      District: district,
      Address:  address,
   }, nil
}
 
// Equals 判断2个地址是否相等
func (addr *Address) Equals(compareAddr *Address) bool {
   if addr.Province == compareAddr.Province && addr.City == compareAddr.City &&
      addr.District == compareAddr.District && addr.Address == compareAddr.Address {
      return true
   }
   return false
}

调整账号聚合根,增加地址(值对象)

// domain/account/account.go
// 账号聚合根
type Account struct {
  ...
  Addr      *Address    // 值对象
}
...
// 更新账号地址
func (a *Account) UpdateAddress(province, city, district, address string) error {
   addr, err := NewAddress(province, city, district, address)
   if err != nil {
      return err
   }
   a.Addr = addr
   return nil
}

账号领域服务增加更新地址

// domain/account/service.go
// 领域服务
...
// 更新地址
func (ds *AccountService) UpdateAddress(ctx context.Context, accountID uint64, province, city, district, address string) error {
   // 读取更新账号
   account, err := ds.accountRepo.GetAccount(ctx, accountID)
   if err != nil {
      return err
   }
 
   if err := account.UpdateAddress(province, city, district, address); err != nil {
      return err
   }
 
   // 存储账号聚合
   return ds.accountRepo.Update(ctx, account)
}

应用服务增加更新入口

// app/account.go
// 账号应用服务
...
// 更新账号地址
func (as *AccountService) UpdateAddress(ctx context.Context, accountID uint64, province, city, district, address string) error {
   if err := as.accountDS.UpdateAddress(ctx, accountID, province, city, district, address); err != nil {
      return err
   }
 
   // 发布事件
   return vars.EventPublisher.Publish(ctx, "account", "update-address", map[string]interface{}{
      "accountID": accountID,
   })
}

3)为账号聚合增加银行卡实体,账号可以增加删除银行卡

银行卡实体

// domain/account/bank_card.go
// 银行卡实体
type BankCard struct {
   ID       string `json:"id"`        // 银行卡号
   BankName string `json:"bank_name"` // 银行名称
   Status   bool   `json:"status"`    // 开启状态
}
 
func NewBankCard(bankNumber string, bankName string) (*BankCard, error) {
   if bankNumber == "" {
      return nil, errors.New("银行号码不能为空")
   }
 
   if bankName == "" {
      return nil, errors.New("银行不能为空")
   }
 
   return &BankCard{
      ID:       bankNumber,
      BankName: bankName,
      Status:   true,
   }, nil
}
 
// 启动银行卡
func (c *BankCard) Enable() error {
   c.Status = true
   return nil
}
 
// 禁用银行卡
func (c *BankCard) Disable() error {
   c.Status = false
   return nil
}

账号聚合调整

// domain/account/account.go
// 账号聚合根
type Account struct {
   ...
   BankCards []*BankCard // 银行卡(实体)
}
...
// 删除银行卡
func (a *Account) RemoveBankCard(bankNumber string) error {
   bankCards := make([]*BankCard, 0)
   for _, bankCard := range a.BankCards {
      if bankCard.ID != bankNumber {
         bankCards = append(bankCards, bankCard)
      }
   }
 
   if len(bankCards) == len(a.BankCards) {
      return errors.New("找不到对应银行卡")
   }
 
   a.BankCards = bankCards
   return nil
}
 
// 增加银行卡
func (a *Account) AddBankCard(bankNumber, bankName string) error {
   // 判断该银行是否已经存在
   for _, bankCard := range a.BankCards {
      if bankCard.ID == bankNumber {
         return errors.New("该银行卡已经存在")
      }
   }
 
   bankCard, err := NewBankCard(bankNumber, bankName)
   if err != nil {
      return err
   }
 
   a.BankCards = append(a.BankCards, bankCard)
   return nil
}
 
// 启用银行卡
func (a *Account) EnableBankCard(bankNumber string) error {
   opBankCard, err := a.getBankCard(bankNumber)
   if err != nil {
      return err
   }
   return opBankCard.Enable()
}
 
// 禁用银行卡
func (a *Account) DisableBankCard(bankNumber string) error {
   opBankCard, err := a.getBankCard(bankNumber)
   if err != nil {
      return err
   }
   return opBankCard.Disable()
}
 
// 读取账号内的bank
func (a *Account) getBankCard(bankNumber string) (*BankCard, error) {
   var opBankCard *BankCard
   for _, bankCard := range a.BankCards {
      if bankCard.ID == bankNumber {
         opBankCard = bankCard
         break
      }
   }
 
   if opBankCard == nil {
      return nil, errors.New("找不到银行卡")
   }
 
   return opBankCard, nil
}

领域服务调整

// domain/account/service.go
// 领域服务
...
// 增加银行卡
func (ds *AccountService) AddBankCard(ctx context.Context, accountID, bankNumber, bankName string) error {
    // 读取更新账号
    account, err := ds.accountRepo.GetAccount(ctx, accountID)
    if err != nil {
        return err   
    }
     
    if err := account.AddBankCard(bankNumber, bankName); err != nil {
        return err   
    }
     
   // 存储账号聚合
   return ds.accountRepo.Update(ctx, account)
}
 
// 移除银行卡
func (ds *AccountService) RemoveBankCard(ctx context.Context, accountID, bankNumber, bankName string) error {
    // 读取更新账号
    account, err := ds.accountRepo.GetAccount(ctx, accountID)
    if err != nil {
        return err   
    }
     
    if err := account.RemoveBankCard(bankNumber, bankName); err != nil {
        return err   
    }
     
   // 存储账号聚合
   return ds.accountRepo.Update(ctx, account)
}
 
// 启用银行卡
func (ds *AccountService) EnableBankCard(ctx context.Context, accountID, bankNumber, bankName string) error {
    // 读取更新账号
    account, err := ds.accountRepo.GetAccount(ctx, accountID)
    if err != nil {
        return err   
    }
     
    if err := account.EnableBankCard(bankNumber, bankName); err != nil {
        return err   
    }
     
   // 存储账号聚合
   return ds.accountRepo.Update(ctx, account)
}
 
// 禁用银行卡
func (ds *AccountService) DisableBankCard(ctx context.Context, accountID, bankNumber, bankName string) error {
    // 读取更新账号
    account, err := ds.accountRepo.GetAccount(ctx, accountID)
    if err != nil {
        return err   
    }
     
    if err := account.DisableBankCard(bankNumber, bankName); err != nil {
        return err   
    }
     
   // 存储账号聚合
   return ds.accountRepo.Update(ctx, account)
}

应用服务调整

// app/account.go
// 账号应用服务
...
// 增加银行卡
func (as *AccountService) AddBankCard(ctx context.Context, accountID uint64, bankNumber, bankName string) error {
   if err := as.accountDS.AddBankCard(ctx, accountID, bankNumber, bankName); err != nil {
      return err
   }
 
   // 发布事件
   return vars.EventPublisher.Publish(ctx, "account", "add-bank-card", map[string]interface{}{
      "accountID":  accountID,
      "bankNumber": bankNumber,
   })
}
 
// 删除银行卡
func (as *AccountService) RemoveBankCard(ctx context.Context, accountID uint64, bankNumber string) error {
   if err := as.accountDS.RemoveBankCard(ctx, accountID, bankNumber); err != nil {
      return err
   }
 
   // 发布事件
   return vars.EventPublisher.Publish(ctx, "account", "remove-bank-card", map[string]interface{}{
      "accountID":  accountID,
      "bankNumber": bankNumber,
   })
}
 
// 启用银行卡
func (as *AccountService) EnableBankCard(ctx context.Context, accountID uint64, bankNumber string) error {
   return as.accountDS.EnableBankCard(ctx, accountID, bankNumber)
}
 
// 禁用银行卡
func (as *AccountService) DisableBankCard(ctx context.Context, accountID uint64, bankNumber string) error {
   return as.accountDS.DisableBankCard(ctx, accountID, bankNumber)
}

回顾 聚合与聚合根

聚合(Aggreate)是一系列由相关的事物组合,它可以作为一个状态变更单位。每个Aggreate都需要一个实体(Entity)作为它的聚合根(Aggregate Root), 并且只有一个聚合根,任何的改变都通过聚合根进行传递,再到内部对应的实体或值对象操作。

  • 聚合(Aggregate)的设计原则

    • 聚合根(Aggregate Root)的实体(Entity)必须在Bounded Context中有唯一的标识性,它的ID不能与其他的聚合根(Aggregate Root)重复
    • 聚合根(Aggregate Root)负责检测边界内所有固定规则
    • 外部无法直接引用聚合内的实体或值对象
    • 聚合根(Aggregate Root)才能通过Repo查询获取,其他内部实体或值对象都需要通过聚合根(Aggregate Root)才能获取
    • 聚合只能引用其他聚合根(Aggregate Root)的ID
    • 删除聚合时,必须连同内部的实体和值对象
    • 当保存聚合时,也必须一并保存内部的实体和值对象(即聚合根内就是一个整体)
  • 关于聚合存储问题

    • 在存储聚合时,一次性将聚合内的关联的实体、值对象写入数据库中,需要锁住聚合内相关的表(Table)、总感觉很耗费性能以及不切实际的,特殊仓储层为Mysql时。但是为了保证统一生命周期内只有一个聚合存在的原则,我们在设计聚合时应该尽量的找小的聚合,这也是一种优化手段。
  • 如何找到聚合

    • 第一步:先找出大聚合

      • 根据逻辑与业务问题、我们很容易先将一个业务包成一个大聚合,但是没有关系,找出聚合之后才进行拆分。
      • 以商城为例,最容易得到的聚合就是商店(Shop)这个大聚合,里面包含有Order、Product、Discount等实体。
      • 当我们需要更新Shop的标题、简介的信息时,为了保证唯一性,就连其他不更新的实体的表(Table)也要锁住,这时创建订单都无法执行,需要等待商品(shop)更新完成。
    • 第二步:大聚合里分小聚合

      • 聚合(Aggregate)越大、复杂度越低、性能越差。聚合(Aggregate)越小、复杂度越高、性能越好。
      • 有了大聚合之后,可以通过更多的案例对聚合做更多分析及设计。特别注意那些由两个以上使用者同时修改一个聚合(Aggregate)的情况
      • 以商城为例,订单是属于用户的,同时订单也会给用户奖励积分,于是将Order归到User这个聚合下,这样就会出现在更新User积分的情况下,当前用户无法下单,这说明聚合拆得不够细。因此我们又将Order和User分开成2个聚合,但是Order这个聚合会带有UserID来保持与User的关系引用。

实战痛点汇总: 聚合这个概念在MongoDB里操作是完全没问题的,一个聚合可以当成一个document,操作完直接存储即可。但由于我们使用的Mysql,所以会出现聚合里面的实体、属性等分散在MySQL的不同表中,保存上存在一定的难度。

  • 痛点1:当账号中只更新了amount字段时,如何在众多字段中识别出来
  • 痛点2:在账号中的银行卡内容更新时,如何存储银行卡
  • 痛点3:在账号中的银行卡操作时,如何银行的状态变更如何与增加与删除识别
  • 痛点4:当账号中银行的数量很多时,当从仓储层(repo)一次加载出来时很慢,并且还存在读取出来后存在只是操作余额的情况

痛点1、2、3的通性就是储存问题,一个聚合单个属性、一个是聚合关联实体数据、一个聚合关联实体的变更数据。
痛点1、痛点2:解决当前的问题可以引入Attribute结构体,为聚合增加一个隐性属性,用于存储变更的数据

// common/pkg/attr/attr.go
package attr
 
type attributer interface {
   Set(string, interface{})
   Attributes() map[string]interface{}
   SetAttributes(attrs map[string]interface{})
   Refresh()
}
 
type Attribute struct {
   attributes map[string]interface{}
}
 
func (a *Attribute) Set(f string, v interface{}) {
   if a.attributes == nil {
      a.attributes = make(map[string]interface{})
   }
 
   a.attributes[f] = v
}
 
func (a *Attribute) SetAttributes(attrs map[string]interface{}) {
   a.attributes = attrs
}
 
func (a *Attribute) Refresh() {
   a.attributes = nil
}
 
func (a *Attribute) Attributes() map[string]interface{} {
   return a.attributes
}

调整账号聚合

// domain/account/account.go
// 账号聚合根
type Account struct {
    ....
    changes   attr.Attribute
}
 
// 扣除金额
func (a *Account) DecreaseBalance(amount float32) error {
   if amount < 0 {
      return errors.New("扣除金额必须大于0")
   }
   if amount > a.Amount {
      return errors.New("账户余额不足")
   }
   a.Amount = a.Amount - amount
   a.changes.Set("amount", a.Amount)    // 新增
   return nil
}
 
// 增加金额
func (a *Account) IncreaseBalance(amount float32) error {
   if amount < 0 {
      return errors.New("增加余额必须大于0")
   }
   a.Amount = a.Amount + amount
   a.changes.Set("amount", a.Amount)    // 新增
   return nil
}
 
// 更新账号地址
func (a *Account) UpdateAddress(province, city, district, address string) error {
   addr, err := NewAddress(province, city, district, address)
   if err != nil {
      return err
   }
   a.Addr = addr
   // ============================ 新增start
   a.changes.Set("province", province)
   a.changes.Set("city", city)
   a.changes.Set("district", district)
   a.changes.Set("address", address)
  // ============================ 新增end
   return nil
}
 
// 获取变动参数
func (a *Account) GetChanges() map[string]interface{} {
   changes := a.changes.Attributes()
   a.changes.Refresh()
   return changes
}

仓储层相关代码

当前Account聚合在mysql的表存储分为account和bank_card表

  • account表:属性 id, amount, province, city, district, address
  • bank_card表:属性 id, account_id, bank_name, status

// infra/repository/mysql/domain/account.go
// 账号仓储层实现
type AccountRepo struct {   
}
 
func (a *AccountRepo) Update(ctx context.Context, account *account.Account) error {
   dao := itool.GetDBWithContext(ctx)
 
   accountChanges := account.GetChanges()
   if len(accountChanges) > 0 { // 只更新变动的
      if err := dao.Model(&model.Account{}).Where("id = ?", account.ID).Updates(accountChanges).Error; err != nil {
         return err
      }
   }
   ...
 
   return nil
}
 
// infra/repository/mysql/model/account.go
type Account struct {
   ID       uint64  `json:"id"`
   Amount   float32 `json:"amount"`
   Province string  `json:"province"` // 省
   City     string  `json:"city"`     // 市
   District string  `json:"district"` // 区
   Address  string  `json:"address"`  // 地址
}
 
func (Account) TableName() string {
   return "account"
}

痛点3:只需仓储层处理该问题即可,痛点2的改动也在此展示


// domain/account/bank_cardgo
// 银行卡实体
type BankCard struct {
    ...
    changes attr.Attribute
}
...
 
// 启动银行卡
func (c *BankCard) Enable() error {
    c.Status = true
    this.changes.Set("status", "1")
    return nil
}
// 禁用银行卡
func (c *BankCard) Disable() error {
    c.Status = false
    this.changes.Set("status", "0")
    return nil
}
 
fun(a *Account) GetChanges() map[string]interface{}{
    changes := a.changes.Attributes()
    a.changes.Refresh()
    return changes
}
// infra/repository/mysql/domain/account.go
// 账号仓储层实现
type AccountRepo struct {   
}
 
func (a *AccountRepo) Update(ctx context.Context, account *account.Account) error {
   dao := itool.GetDBWithContext(ctx)
 
   accountChanges := account.GetChanges()
   if len(accountChanges) > 0 { // 只更新变动的
      if err := dao.Model(&model.Account{}).Where("id = ?", account.ID).Updates(accountChanges).Error; err != nil {
         return err
      }
   }
 
   // 处理银行卡的更新
   if err := a.handleBankCards(ctx, account); err != nil {
      return err
   }
 
   return nil
}
 
// 处理银行卡
func (a *AccountRepo) handleBankCards(ctx context.Context, account *account.Account) error {
   ...
   return a.updateBankCards(ctx, account)
}
// updateBankCards 更新银行卡
func (a *AccountRepo) updateBankCards(ctx context.Context, account *account.Account) error {
   dao := itool.GetDBWithContext(ctx)
   for _, bankCard := range account.BankCards {
      bankCardChanges := bankCard.GetChanges()
      if len(bankCardChanges) > 0 {
         if err := dao.Model(&model.BankCard{}).Where("id = ?", bankCard.ID).Updates(bankCardChanges).Error; err != nil {
            return err
         }
      }
   }
 
   return nil
}
 
// infra/repository/mysql/model/bank_card.go
// 银行卡模型
type BankCard struct {
   ID        string `json:"id"`         // 银行卡号
   AccountID uint64 `json:"account_id"` // 账号ID
   BankName  string `json:"bank_name"`  // 银行名称
   Status    bool   `json:"status"`     // 开启状态
}
 
func (BankCard) TableName() string {
   return "bank_card"
}

3) 深入思考,当Account聚合中BankCards这类的属性的数量很大或BankCards这类的变动场景很多,经常需要跟Account聚合并发处理,这时就会出现银行卡在更新时,转账等服务就无法处理了,此时就可以将BankCard抽取为一个独立的聚合, 也就是将大聚合拆成2小聚合

案例:账号A花费了10元,购买了10积分到积分账号
增加integral聚合

// domain/integral/account.go
// 积分聚合根
type Integral struct {
   ID       uint64  // ID
   Integral float32 // 积分
   changes  attr.Attribute
}
 
// 扣除积分
func (i *Integral) DecreaseIntegral(integral float32) error {
   if integral < 0 {
      return errors.New("扣除积分必须大于0")
   }
   if integral > i.Integral {
      return errors.New("账户积分不足")
   }
   i.Integral = i.Integral - integral
   i.changes.Set("integral", i.Integral)
   return nil
}
 
// 增加积分
func (i *Integral) IncreaseIntegral(integral float32) error {
   if integral < 0 {
      return errors.New("增加积分必须大于0")
   }
   i.Integral = i.Integral + integral
   // 追加amount的调整属性
   i.changes.Set("integral", i.Integral)
   return nil
}
 
// 获取变动参数
func (i *Integral) GetChanges() map[string]interface{} {
   changes := i.changes.Attributes()
   i.changes.Refresh()
   return changes
}

新增积分领域服务

// domain/integral/service.go
// 积分领域服务
type IntegralServiceIFace interface {
   IncreaseIntegral(ctx context.Context, integralID uint64, integral float32) error
}
 
type IntegralService struct {
   integralRepo IntegralRepoIFace
}
 
func NewIntegralService(integralRepo IntegralRepoIFace) *IntegralService {
   return &IntegralService{
      integralRepo: integralRepo,
   }
}
 
// 增加积分
func (ds *IntegralService) IncreaseIntegral(ctx context.Context, integralID uint64, integral float32) error {
   if integral <= 0 {
      return errors.New("增加积分不能小于0")
   }
 
   // 读取积分聚合
   integralAggr, err := ds.integralRepo.GetIntegral(ctx, integralID)
   if err != nil {
      return err
   }
 
   // 增加积分
   if err := integralAggr.IncreaseIntegral(integral); err != nil {
      return err
   }
 
   // 积分聚合落库
   return ds.integralRepo.Update(ctx, integralAggr)
}

调整账号领域服务

// domain/account/service.go
// 领域服务
type AccountService struct {
    ...
}
...
 
// 扣除账号金额
func (ds *AccountService) DecreaseBalance(ctx context.Context, accountID uint64, amount float32) error {
   if amount <= 0 {
      return errors.New("扣除金额不能小于0")
   }
 
   // 读取账号A
   accountAggr, err := ds.accountRepo.GetAccount(ctx, accountID)
   if err != nil {
      return err
   }
 
   // 减少账号A的金额
   if err := accountAggr.DecreaseBalance(amount); err != nil {
      return err
   }
 
   return ds.accountRepo.Update(ctx, accountAggr)
}

账号应用服务调整

// app/account.go
// 账号应用服务
type AccountService struct {
   ...
   integralDS  integral.IntegralServiceIFace
   accountRepo repository.AccountRepoIFace
}
...
 
// 余额购买积分
func (as *AccountService) BuyIntegral(ctx context.Context, accountID, integralID uint64, amount float32) error {
   if err := as.accountRepo.Translation(ctx, func(newCtx context.Context) error {
      // 扣除账号金额
      if err := as.accountDS.DecreaseBalance(newCtx, accountID, amount); err != nil {
         return err
      }
 
      // 增加积分金额
      if err := as.integralDS.IncreaseIntegral(newCtx, integralID, amount); err != nil {
         return err
      }
 
      return nil
   }); err != nil {
      return err
   }
 
   // 发布事件
   return vars.EventPublisher.Publish(ctx, "account", "buy", map[string]interface{}{
      "accountID":  accountID,
      "integralID": integralID,
      "amount":     amount,
   })
}

相对于相互转账,余额购买积分是在应用层开启了全局事务,然后再编排不同的领域服务,达到跨领域的效果

思考:

1)理论上某段时间内,同一个聚合仅会出现一次(像直接account的里面的钱,直接设置了,保存的时候存在已经被修改过了),如何利用代码去保证?

2)APP调用多领域时,可以看到在业务阶段非存库阶段就已经开启了事务,是否有必要优化?

3)无论是领域事件还是应用层事件,都是没有事务消息的,这样是会导致消息丢失,如何有效避免?
4)New一个聚合的时候,传值是应该一个一个传进去,还是可以先把聚合当成结构体,再传值?

5)在账号聚合添加银行卡的时候是否可以直接返回增加或者删除的数据,是否可以变得更优化(即聚合里面可以返回实体,值对象,然后直接再进行持久化?

6)应有服务是否可以直接通过repo读取聚合内一个实体或值对象,还是必须通过领域服务做中转?
7)Domain层要判断一个客户是否存在,是否可以直接通过repo查询?还是一定要拿出聚合再进行判断?
8)APP层通过client(grpc/http)读取的数据,要进行判断等是否要放入domain层?

二、DDD各层标准接口

用户接口层(Server层)

  • server

    • Server层负责校验DTO数据的合法性
  • server_assembler

    • Server层负责接收数据并转换成DTO与APP层交互

应用服务层(App层)

  • service:应用服务

    • 主要编排领域服务,有业务逻辑尽量往领域服务下沉
    • 可直接通过repo获取DO(领域对象)
    • 与领域无关的数据可由应用服务service直接提供服务(如获取成员下有多少个客户)
    • 当领域服务有返回对应的DO时,落库步骤由service层处理
    • 获取第三方服务数据并中转到领域服务中处理
    • 获取不相关聚合并中转到领域服务中处理
    • 当Service要通过Repo读取聚合时,可以将领域实现的repo注入进来
  • dto:数据传输层

    • 定义为外部提供(如用户接口层)的结构体及基础验证
  • client:外部调用接口定义

    • 调用第三方grpc/api的定义
    • 返回的结构体在Interface定义,不要返回proto结构体
  • service_assembler:应用服务转换层

    • 将领域的相关数据组合转换成DTO
  • repository:应用服务仓储服务

    • 定义为service提供服务的仓储接口

领域层(Domain)

  • service:领域服务

    • 提供业务逻辑判断,编排聚合,实现更多业务逻辑
    • 非复杂多聚合交互的情况下,直接在当前落库
    • 在复杂多聚合交互的情况下,需要将聚合返回到应用服务,由应用服务统一落库
    • 禁止直接获取其他领域聚合
  • aggr:聚合根

    • 聚合根下的实体的业务动作由聚合根对外暴露,外部无法直接调用实体
    • 聚合根提供是一系列的业务
    • 同一生命周期只能存在一个聚合根
    • 聚合根为一个单位,无论是读取还是存储都应该当成一个整体去操作,而不是按照表的设计去思考
    • 当聚合根调整的字段有像金额这种累加的情况下,需要在仓储层特殊处理,否则会出现覆盖的情况
    • 当聚合字段过多,可通过attribute记录有修改的数据,优化减少更新的字段
    • 当聚合过大或关联实体有过多操作时,应该考虑拆成更小的聚合,让其更加独立而无不影响
    • 聚合不要调仓储
  • entity:实体

    • 在整个生命周期内只能存在一个
    • 实体的方法由聚合根暴露调用
  • 值对象

    • 值对象作为属性,没有唯一标识
    • 更新值对象只能整个值对象替换
    • 当值对象的所有值相等时,既这2个值对象相等

基础层(Infra)

  • repository:仓储层实现层

    • 无论底层是MSQL还是MongoDB,无论表时怎么拆分存储的,读取一个聚合时,必须是一个完整的聚合
    • 聚合存储时,都是按照一个聚合一个是一个事务处理,聚合内的数据全部失败或成功
    • 当聚合关联时多个实体,需要对实体的删除对比、增加、编辑等做处理
    • 从仓储读取返回只能为一个聚合或直接的接口定义结构体

关于GO与BFF之间的职责定位:

1) BFF不再负责复杂逻辑相关的业务,主要简单的逻辑和拼接数据,尽量让BFF变轻
2) 相关有关联性的判断,交由GO后端判断