百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 热门文章 > 正文

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

bigegpt 2025-05-27 12:46 8 浏览

以下文章来源于从菜鸟到大佬 ,作者八叉树

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

Repository

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

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

资源库对外部是屏蔽了存储细节的,资源库内部去处理 cache、es、db。

数据操作流程

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和创建,在交给资源库去持久化,这也是<<实现领域驱动设计>>的作者推荐的方式,但这种方式更适合文档型数据库,唯一ID是Key和实体序列化是值。

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

相关推荐

【Docker 新手入门指南】第十章:Dockerfile

Dockerfile是Docker镜像构建的核心配置文件,通过预定义的指令集实现镜像的自动化构建。以下从核心概念、指令详解、最佳实践三方面展开说明,帮助你系统掌握Dockerfile的使用逻...

Windows下最简单的ESP8266_ROTS_ESP-IDF环境搭建与腾讯云SDK编译

前言其实也没啥可说的,只是我感觉ESP-IDF对新手来说很不友好,很容易踩坑,尤其是对业余DIY爱好者搭建环境非常困难,即使有官方文档,或者网上的其他文档,但是还是很容易踩坑,多研究,记住两点就行了,...

python虚拟环境迁移(python虚拟环境conda)

主机A的虚拟环境向主机B迁移。前提条件:主机A和主机B已经安装了virtualenv1.主机A操作如下虚拟环境目录:venv进入虚拟环境:sourcevenv/bin/active(1)记录虚拟环...

Python爬虫进阶教程(二):线程、协程

简介线程线程也叫轻量级进程,它是一个基本的CPU执行单元,也是程序执行过程中的最小单元,由线程ID、程序计数器、寄存器集合和堆栈共同组成。线程的引入减小了程序并发执行时的开销,提高了操作系统的并发性能...

基于网络安全的Docker逃逸(docker)

如何判断当前机器是否为Docker容器环境Metasploit中的checkcontainer模块、(判断是否为虚拟机,checkvm模块)搭配学习教程1.检查根目录下是否存在.dockerenv文...

Python编程语言被纳入浙江高考,小学生都开始学了

今年9月份开始的新学期,浙江省三到九年级信息技术课将同步替换新教材。其中,新初二将新增Python编程课程内容。新高一信息技术编程语言由VB替换为Python,大数据、人工智能、程序设计与算法按照教材...

CentOS 7下安装Python 3.10的完整过程

1.安装相应的编译工具yum-ygroupinstall"Developmenttools"yum-yinstallzlib-develbzip2-develope...

如何在Ubuntu 20.04上部署Odoo 14

Odoo是世界上最受欢迎的多合一商务软件。它提供了一系列业务应用程序,包括CRM,网站,电子商务,计费,会计,制造,仓库,项目管理,库存等等,所有这些都无缝集成在一起。Odoo可以通过几种不同的方式进...

Ubuntu 系统安装 PyTorch 全流程指南

当前环境:Ubuntu22.04,显卡为GeForceRTX3080Ti1、下载显卡驱动驱动网站:https://www.nvidia.com/en-us/drivers/根据自己的显卡型号和...

spark+python环境搭建(python 环境搭建)

最近项目需要用到spark大数据相关技术,周末有空spark环境搭起来...目标spark,python运行环境部署在linux服务器个人通过vscode开发通过远程python解释器执行代码准备...

centos7.9安装最新python-3.11.1(centos安装python环境)

centos7.9安装最新python-3.11.1centos7.9默认安装的是python-2.7.5版本,安全扫描时会有很多漏洞,比如:Python命令注入漏洞(CVE-2015-2010...

Linux系统下,五大步骤安装Python

一、下载Python包网上教程大多是通过官方地址进行下载Python的,但由于国内网络环境问题,会导致下载很慢,所以这里建议通过国内镜像进行下载例如:淘宝镜像http://npm.taobao.or...

centos7上安装python3(centos7安装python3.7.2一键脚本)

centos7上默认安装的是python2,要使用python3则需要自行下载源码编译安装。1.安装依赖yum-ygroupinstall"Developmenttools"...

利用本地数据通过微调方式训练 本地DeepSeek-R1 蒸馏模型

网络上相应的教程基本都基于LLaMA-Factory进行,本文章主要顺着相应的教程一步步实现大模型的微调和训练。训练环境:可自行定义,mac、linux或者window之类的均可以,本文以ma...

【法器篇】天啦噜,库崩了没备份(天啦噜是什么意思?)

背景数据库没有做备份,一天突然由于断电或其他原因导致无法启动了,且设置了innodb_force_recovery=6都无法启动,里面的数据怎么才能恢复出来?本例采用解析建表语句+表空间传输的方式进行...