为 Gopher 打造 DDD 系列:领域模型-资源库
bigegpt 2025-05-27 12:46 15 浏览
以下文章来源于从菜鸟到大佬 ,作者八叉树
前言: 作为领域模型中最重要的环节之一的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
相关推荐
- pyproject.toml到底是什么东西?(py trim)
-
最近,在Twitter上有一个Python项目的维护者,他的项目因为构建失败而出现了一些bug(这个特别的项目不提供wheel,只提供sdist)。最终,发现这个bug是由于这个项目使用了一个pypr...
- BDP服务平台SDK for Python3发布(bdp数据平台)
-
下载地址https://github.com/imysm/opends-sdk-python3.git说明最近在开发和bdp平台有关的项目,用到了bdp的python的sdk,但是官方是基于p...
- Python-for-Android (p4a):(python-for-android p4a windows)
-
一、Python-for-Android(p4a)简介Python-for-Android(p4a),一个强大的开发工具,能够将你的Python应用程序打包成可在Android设备上运行...
- Qt for Python—Qt Designer 概览
-
前言本系列第三篇文章(QtforPython学习笔记—应用程序初探)、第四篇文章(QtforPython学习笔记—应用程序再探)中均是使用纯代码方式来开发PySide6GUI应用程序...
- Python:判断质数(jmu-python-判断质数)
-
#Python:判断质数defisPrime(n):foriinrange(2,n):ifn%i==0:return0re...
- 为什么那么多人讨厌Python(为什么python这么难)
-
Python那么棒,为什么那么多人讨厌它呢?我整理了一下,主要有这些原因:用缩进替代大括号许多人抱怨Python完全依赖于缩进来创建代码块,代码多一点就很难看到函数在哪里结束,那么你就需要把一个函数拆...
- 一文了解 Python 中带有 else 的循环语句 for-else/while-else
-
在本文中,我们将向您介绍如何在python中使用带有else的for/while循环语句。可能许多人对循环和else一起使用感到困惑,因为在if-else选择结构中else正常...
- python的numpy向量化语句为什么会比for快?
-
我们先来看看,python之类语言的for循环,和其它语言相比,额外付出了什么。我们知道,python是解释执行的。举例来说,执行x=1234+5678,对编译型语言,是从内存读入两个shor...
- 开眼界!Python遍历文件可以这样做
-
来源:【公众号】Python技术Python对于文件夹或者文件的遍历一般有两种操作方法,一种是至二级利用其封装好的walk方法操作:import osfor root,d...
- 告别简单format()!Python Formatter类让你的代码更专业
-
Python中Formatter类是string模块中的一个重要类,它实现了Python字符串格式化的底层机制,允许开发者创建自定义的格式化行为。通过深入理解Formatter类的工作原理和使用方法,...
- python学习——038如何将for循环改写成列表推导式
-
在Python里,列表推导式是一种能够简洁生成列表的表达式,可用于替换普通的for循环。下面是列表推导式的基本语法和常见应用场景。基本语法result=[]foriteminite...
- 详谈for循环和while循环的区别(for循环语句与while循环语句有什么区别)
-
初九,潜龙勿用在刚开始使用python循环语句时,经常会遇到for循环和while循环的混用,不清楚该如何选择;今天就对这2个循环语句做深入的分析,让大家更好地了解这2个循环语句以方便后续学习的加深。...
- Python编程基础:循环结构for和while
-
Python中的循环结构包括两个,一是遍历循环(for循环),一是条件循环(while循环)。遍历循环遍历循环(for循环)会挨个访问序列或可迭代对象的元素,并执行里面的代码块。foriinra...
- 学习编程第154天 python编程 for循环输出菱形图
-
今天学习的是刘金玉老师零基础Python教程第38期,主要内容是python编程for循环输出菱形※。(一)利用for循环输出菱形形状的*号图形1.思路:将菱形分解为上下两个部分三角形图案,分别利用...
- python 10个堪称完美的for循环实践
-
在Python中,for循环的高效使用能显著提升代码性能和可读性。以下是10个堪称完美的for循环实践,涵盖数据处理、算法优化和Pythonic编程风格:1.遍历列表同时获取索引(enumerate...
- 一周热门
- 最近发表
-
- pyproject.toml到底是什么东西?(py trim)
- BDP服务平台SDK for Python3发布(bdp数据平台)
- Python-for-Android (p4a):(python-for-android p4a windows)
- Qt for Python—Qt Designer 概览
- Python:判断质数(jmu-python-判断质数)
- 为什么那么多人讨厌Python(为什么python这么难)
- 一文了解 Python 中带有 else 的循环语句 for-else/while-else
- python的numpy向量化语句为什么会比for快?
- 开眼界!Python遍历文件可以这样做
- 告别简单format()!Python Formatter类让你的代码更专业
- 标签列表
-
- mybatiscollection (79)
- mqtt服务器 (88)
- keyerror (78)
- c#map (65)
- xftp6 (83)
- bt搜索 (75)
- c#var (76)
- xcode-select (66)
- mysql授权 (74)
- 下载测试 (70)
- linuxlink (65)
- pythonwget (67)
- androidinclude (65)
- libcrypto.so (74)
- linux安装minio (74)
- ubuntuunzip (67)
- vscode使用技巧 (83)
- secure-file-priv (67)
- vue阻止冒泡 (67)
- jquery跨域 (68)
- php写入文件 (73)
- kafkatools (66)
- mysql导出数据库 (66)
- jquery鼠标移入移出 (71)
- 取小数点后两位的函数 (73)