为 Gopher 打造 DDD 系列:领域模型-资源库

共 6517字,需浏览 14分钟

 ·

2020-07-31 18:22

前言: 作为领域模型中最重要的环节之一的Repository,其通过对外暴露接口屏蔽了内部的复杂性,又有其隐式写时复制的巧妙代码设计,完美的将DDD中的Repository的概念与代码相结合!

Repository

资源库通常标识一个存储的区域,提供读写功能。通常我们将实体存放在资源库中,之后通过该资源库来获取相同的实体,每一个实体都搭配一个资源库。

如果你修改了某个实体,也需要通过资源库去持久化。当然你也可以通过资源库去删除某一个实体。

资源库对外部是屏蔽了存储细节的,资源库内部去处理 cacheesdb数据操作流程

Repository解除了client的巨大负担,使client只需与一个简单的、易于理解的接口进行对话,并根据模型向这个接口提出它的请求。要实现所有这些功能需要大量复杂的技术基础设施,但接口却很简单,而且在概念层次上与领域模型紧密联系在一起。

隐式写时复制

通常我们通过资源库读取一个实体后,再对这个实体进行修改。那么这个修改后的持久化是需要知道实体的哪些属性被修改,然后再对应的去持久化被修改的属性。

注意商品实体的changes,商品被修改某个属性,对应的Repository就持久化相应的修改。这么写有什么好处呢?如果不这么做,那只能在service里调用orm指定更新列,但是这样做的话,Repository的价值就完全被舍弃了!

可以说写时复制是Repository和领域模型的桥梁!

//商品实体type Goods struct {    changes map[string]interface{}        //被修改的属性    Name    string//商品名称  Price   int// 价格  Stock   int// 库存}// SetPrice .func (obj *Goods) SetPrice(price int) {  obj.Price = price  obj.changes["price"] = price //写时复制}
// SetStock .func (obj *Goods) SetStock(stock int) { obj.Stock = stock obj.changes["stock"] = stock //写时复制}
//示例func main() { goodsEntity := GoodsRepository.Get(1) goodsEntity.SetPrice(1000) GoodsRepositorySave(goodsEntity) //GoodsRepository 会内部处理商品实体的changes}

工厂和创建

创建商品实体需要唯一ID和已知的属性名称等,可以使用实体工厂去生成唯一ID和创建,在交给资源库去持久化,这也是<<实现领域驱动设计>>的作者推荐的方式,但这种方式更适合文档型数据库,唯一IDKey和实体序列化是值。

“底层技术可能会限制我们的建模选择。例如,关系数据库可能对复合对象结构的深度有实际的限制"(领域驱动设计:软件核心复杂性应对之道 Eric Evans)

但我们更多的使用的是关系型数据库,这样资源库就需要创建的行为。实体的唯一ID就是聚簇主键。一个实体或许是多张表组成,毕竟我们还要考虑垂直分表。我认为DDD的范式和关系型数据库范式,后者更重要。有时候我们还要为Repository 实现一些统计select count(*)的功能。

根据所使用的持久化技术和基础设施不同,Repository的实现也将有很大的变化。理想的实现是向客户隐藏所有内部工作细节(尽管不向客户的开发人员隐藏这些细节),这样不管数据是存储在对象数据库中,还是存储在关系数据库中,或是简单地保持在内存中,客户代码都相同。Repository将会委托相应的基础设施服务来完成工作。将存储、检索和查询机制封装起来是Repository实现的最基本的特性。

实践

https://github.com/8treenet/freedom/tree/master/example/fshop/adapter/repository

实体的缓存

这个是缓存组件的接口,可以读写实体,实体的key 使用必须实现的Identity 方法。

  • 一级缓存是基于请求的,首先会从一级缓存查找实体,生命周期是一个请求的开始和结束。
  • 二级缓存是基于redis
  • 组件已经做了幂等的防击穿处理
  • SetSource设置持久化的回调函数,当一、二级缓存未命中,会读取回调函数,并反写一、二级缓存。
// freedom.Entitytype Entity interface {  DomainEvent(string, interface{},...map[string]string)  Identity() string  GetWorker() Worker  SetProducer(string)  Marshal() []byte}
// infra.EntityCachetype EntityCache interface { //获取实体 GetEntity(freedom.Entity) error //删除实体缓存 Delete(result freedom.Entity, async ...bool) error //设置数据源 SetSource(func(freedom.Entity) error) EntityCache //设置前缀 SetPrefix(string) EntityCache //设置缓存时间,默认5分钟 SetExpiration(time.Duration) EntityCache //设置异步反写缓存。默认关闭,缓存未命中读取数据源后的异步反写缓存 SetAsyncWrite(bool) EntityCache //设置防击穿,默认开启 SetSingleFlight(bool) EntityCache //关闭二级缓存. 关闭后只有一级缓存生效 CloseRedis() EntityCache}

以下实现了一个商品的资源库
package repository
import ( "time"
"github.com/8treenet/freedom/infra/store"
"github.com/8treenet/freedom/example/fshop/domain/po" "github.com/8treenet/freedom/example/fshop/domain/entity"
"github.com/8treenet/freedom")
func init() { freedom.Prepare(func(initiator freedom.Initiator) { initiator.BindRepository(func() *Goods { return &Goods{} }) })}
// Goods .type Goods struct { freedom.Repository //资源库必须继承,这样是为了约束 db、redis、http等的访问 Cache store.EntityCache //依赖注入实体缓存组件}
// BeginRequestfunc (repo *Goods) BeginRequest(worker freedom.Worker) { repo.Repository.BeginRequest(worker)
//设置缓存的持久化数据源,旁路缓存模型,如果缓存未有数据,将回调该函数。 repo.Cache.SetSource(func(result freedom.Entity) error { return findGoods(repo, result) }) //缓存30秒, 不设置默认5分钟 repo.Cache.SetExpiration(30 * time.Second) //设置缓存前缀 repo.Cache.SetPrefix("freedom")}
// Get 通过id 获取商品实体.func (repo *Goods) Get(id int) (goodsEntity *entity.Goods, e error) { goodsEntity = &entity.Goods{} goodsEntity.Id = id //注入基础Entity 包含运行时和领域事件的producer repo.InjectBaseEntity(goodsEntity)
//读取缓存, Identity() 会返回 id,缓存会使用它当key return goodsEntity, repo.Cache.GetEntity(goodsEntity)}
// Save 持久化实体.func (repo *Goods) Save(entity *entity.Goods) error { _, e := saveGoods(repo, entity) //写库,saveGoods是脚手架生成的函数,会做写时复制的处理。 //清空缓存 repo.Cache.Delete(entity) return e}
func (repo *Goods) FindsByPage(page, pageSize int, tag string) (entitys []*entity.Goods, e error) { build := repo.NewORMDescBuilder("id").NewPageBuilder(page, pageSize) //创建分页器 e = findGoodsList(repo, po.Goods{Tag: tag}, &entitys, build) if e != nil { return } //注入基础Entity 包含运行时和领域事件的producer repo.InjectBaseEntitys(entitys) return}
func (repo *Goods) New(name, tag string, price, stock int) (entityGoods *entity.Goods, e error) { goods := po.Goods{Name: name, Price: price, Stock: stock, Tag: tag, Created: time.Now(), Updated: time.Now()}
_, e = createGoods(repo, &goods) //写库,createGoods是脚手架生成的函数。 if e != nil { return } entityGoods = &entity.Goods{Goods: goods} repo.InjectBaseEntity(entityGoods) return}

领域服务使用仓库
package domain
import ( "github.com/8treenet/freedom/example/fshop/domain/dto" "github.com/8treenet/freedom/example/fshop/adapter/repository" "github.com/8treenet/freedom/example/fshop/domain/aggregate" "github.com/8treenet/freedom/example/fshop/domain/entity" "github.com/8treenet/freedom/infra/transaction"
"github.com/8treenet/freedom")
func init() { freedom.Prepare(func(initiator freedom.Initiator) { initiator.BindService(func() *Goods { return &Goods{} }) initiator.InjectController(func(ctx freedom.Context) (service *Goods) { initiator.GetService(ctx, &service) return }) })}
// Goods 商品领域服务.type Goods struct { Worker freedom.Worker //依赖注入请求运行时对象。 GoodsRepo repository.Goods //依赖注入商品仓库}
// New 创建商品func (g *Goods) New(name string, price int) (e error) { g.Worker.Logger().Info("创建商品") _, e = g.GoodsRepo.New(name, entity.GoodsNoneTag, price, 100) return}
// Items 分页商品列表func (g *Goods) Items(page, pagesize int, tag string) (items []dto.GoodsItemRes, e error) { entitys, e := g.GoodsRepo.FindsByPage(page, pagesize, tag) if e != nil { return }
for i := 0; i < len(entitys); i++ { items = append(items, dto.GoodsItemRes{ Id: entitys[i].Id, Name: entitys[i].Name, Price: entitys[i].Price, Stock: entitys[i].Stock, Tag: entitys[i].Tag, }) } return}
// AddStock 增加商品库存func (g *Goods) AddStock(goodsId, num int) (e error) { entity, e := g.GoodsRepo.Get(goodsId) if e != nil { return }
entity.AddStock(num) //增加库存 entity.DomainEvent("Goods.Stock", entity) //发布增加商品库存的领域事件 return g.GoodsRepo.Save(entity)}

项目代码 https://github.com/8treenet/freedom/tree/master/example/fshop




推荐阅读



学习交流 Go 语言,扫码回复「进群」即可


站长 polarisxu

自己的原创文章

不限于 Go 技术

职场和创业经验


Go语言中文网

每天为你

分享 Go 知识

Go爱好者值得关注


浏览 42
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报