Golang领域模型-资源库

{"type":"doc","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"前言:"},{"type":"text","text":" 作为领域模型中最重要的环节之一的Repository,其通过对外暴露接口屏蔽了内部的复杂性,又有其"},{"type":"text","marks":[{"type":"strong"}],"text":"隐式写时复制"},{"type":"text","text":"的巧妙代码设计,完美的将DDD中的Repository的概念与代码相结合!"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"Repository"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"资源库通常标识一个存储的区域,提供读写功能。通常我们将实体存放在资源库中,之后通过该资源库来获取相同的实体,每一个实体都搭配一个资源库。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如果你修改了某个实体,也需要通过资源库去持久化。当然你也可以通过资源库去删除某一个实体。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"资源库对外部是屏蔽了存储细节的,资源库内部去处理 cache、es、db。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/17/17b7439b88d0d4dc746965ca707e8470.png","alt":null,"title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":"center","origin":null},"content":[{"type":"text","marks":[{"type":"size","attrs":{"size":10}}],"text":"操作流程"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Repository解除了client的巨大负担,使client只需与一个简单的、易于理解的接口进行对话,并根据模型向这个接口提出它的请求。要实现所有这些功能需要大量复杂的技术基础设施,但接口却很简单,而且在概念层次上与领域模型紧密联系在一起。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"隐式写时复制"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"通常我们通过资源库读取一个实体后,再对这个实体进行修改。那么这个修改后的持久化是需要知道实体的哪些属性被修改,然后再对应的去持久化被修改的属性。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"注意商品实体的changes"},{"type":"text","text":",商品被修改某个属性,对应的Repository就持久化相应的修改。 这么写有什么好处呢?如果不这么做,那只能在service里调用orm指定更新列,但是这样做的话,Repository的价值就完全被舍弃了!"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"可以说写时复制是Repository和领域模型的桥梁!"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"//商品实体\ntype Goods struct {\n changes map[string]interface{} //被修改的属性\n Name string //商品名称\n Price int // 价格\n Stock int // 库存\n}\n// SetPrice .\nfunc (obj *Goods) SetPrice(price int) {\n obj.Price = price\n obj.changes[\"price\"] = price //写时复制\n}\n\n// SetStock .\nfunc (obj *Goods) SetStock(stock int) {\n obj.Stock = stock\n obj.changes[\"stock\"] = stock //写时复制\n}\n\n//示例\nfunc main() {\n goodsEntity := GoodsRepository.Get(1) \n goodsEntity.SetPrice(1000)\n GoodsRepositorySave(goodsEntity) //GoodsRepository 会内部处理商品实体的changes\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"工厂和创建"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"创建商品实体需要唯一ID和已知的属性名称等,可以使用实体工厂去生成唯一ID和创建,在交给资源库去持久化,这也是<>的作者推荐的方式,但这种方式更适合文档型数据库,唯一ID是Key和实体序列化是值。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"“底层技术可能会限制我们的建模选择。例如,关系数据库可能对复合对象结构的深度有实际的限制\"(领域驱动设计:软件核心复杂性应对之道 Eric Evans)"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"但我们更多的使用的是关系型数据库,这样资源库就需要创建的行为。实体的唯一ID就是聚簇主键。一个实体或许是多张表组成,毕竟我们还要考虑垂直分表。我认为DDD的范式和关系型数据库范式,后者更重要。有时候我们还要为Repository 实现一些统计select count(*)的功能。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"根据所使用的持久化技术和基础设施不同,Repository的实现也将有很大的变化。理想的实现是向客户隐藏所有内部工作细节(尽管不向客户的开发人员隐藏这些细节),这样不管数据是存储在对象数据库中,还是存储在关系数据库中,或是简单地保持在内存中,客户代码都相同。Repository将会委托相应的基础设施服务来完成工作。将存储、检索和查询机制封装起来是Repository实现的最基本的特性。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"实践"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"link","attrs":{"href":"https://github.com/8treenet/freedom/tree/master/example/fshop/adapter/repository","title":null},"content":[{"type":"text","text":"https://github.com/8treenet/freedom/tree/master/example/fshop/adapter/repository"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"实体的缓存"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"这个是缓存组件的接口,可以读写实体,实体的key 使用必须实现的Identity 方法。 - 一级缓存是基于请求的,首先会从一级缓存查找实体,生命周期是一个请求的开始和结束。 - 二级缓存是基于redis。 - 组件已经做了"},{"type":"text","marks":[{"type":"strong"}],"text":"幂等的防击穿处理"},{"type":"text","text":"。 - SetSource设置持久化的回调函数,当一、二级缓存未命中,会读取回调函数,并反写一、二级缓存。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"// freedom.Entity\ntype Entity interface {\n DomainEvent(string, interface{},...map[string]string)\n Identity() string\n GetWorker() Worker\n SetProducer(string)\n Marshal() []byte\n}\n\n// infra.EntityCache\ntype EntityCache interface {\n //获取实体\n GetEntity(freedom.Entity) error\n //删除实体缓存\n Delete(result freedom.Entity, async ...bool) error\n //设置数据源\n SetSource(func(freedom.Entity) error) EntityCache\n //设置前缀\n SetPrefix(string) EntityCache\n //设置缓存时间,默认5分钟\n SetExpiration(time.Duration) EntityCache\n //设置异步反写缓存。默认关闭,缓存未命中读取数据源后的异步反写缓存\n SetAsyncWrite(bool) EntityCache\n //设置防击穿,默认开启\n SetSingleFlight(bool) EntityCache\n //关闭二级缓存. 关闭后只有一级缓存生效\n CloseRedis() EntityCache\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"以下实现了一个商品的资源库"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"package repository\n\nimport (\n \"time\"\n\n \"github.com/8treenet/freedom/infra/store\"\n\n \"github.com/8treenet/freedom/example/fshop/domain/po\"\n \"github.com/8treenet/freedom/example/fshop/domain/entity\"\n\n \"github.com/8treenet/freedom\"\n)\n\nfunc init() {\n freedom.Prepare(func(initiator freedom.Initiator) {\n //绑定创建资源库函数到框架,框架会根据客户的使用做依赖倒置和依赖注入的处理。\n initiator.BindRepository(func() *Goods {\n //创建 Goods资源库\n return &Goods{}\n })\n })\n}\n// Goods .\ntype Goods struct {\n freedom.Repository //资源库必须继承,这样是为了约束 db、redis、http等的访问\n Cache store.EntityCache //依赖注入实体缓存组件\n}\n\n// BeginRequest\nfunc (repo *Goods) BeginRequest(worker freedom.Worker) {\n repo.Repository.BeginRequest(worker)\n\n //设置缓存的持久化数据源,旁路缓存模型,如果缓存未有数据,将回调该函数。\n repo.Cache.SetSource(func(result freedom.Entity) error {\n\nreturn findGoods(repo, result) \n })\n //缓存30秒, 不设置默认5分钟\n repo.Cache.SetExpiration(30 * time.Second)\n //设置缓存前缀\n repo.Cache.SetPrefix(\"freedom\")\n}\n\n// Get 通过id 获取商品实体.\nfunc (repo *Goods) Get(id int) (goodsEntity *entity.Goods, e error) {\n goodsEntity = &entity.Goods{}\n goodsEntity.Id = id\n //注入基础Entity 包含运行时和领域事件的producer\n repo.InjectBaseEntity(goodsEntity)\n\n //读取缓存, Identity() 会返回 id,缓存会使用它当key\n return goodsEntity, repo.Cache.GetEntity(goodsEntity)\n}\n\n// Save 持久化实体.\nfunc (repo *Goods) Save(entity *entity.Goods) error {\n _, e := saveGoods(repo, entity) //写库,saveGoods是脚手架生成的函数,会做写时复制的处理。\n //清空缓存\n repo.Cache.Delete(entity)\n return e\n}\n\nfunc (repo *Goods) FindsByPage(page, pageSize int, tag string) (entitys []*entity.Goods, e error) {\n build := repo.NewORMDescBuilder(\"id\").NewPageBuilder(page, pageSize) //创建分页器\n e = findGoodsList(repo, po.Goods{Tag: tag}, &entitys, build)\n if e != nil {\n return\n }\n //注入基础Entity 包含运行时和领域事件的producer\n repo.InjectBaseEntitys(entitys)\n return\n}\n\nfunc (repo *Goods) New(name, tag string, price, stock int) (entityGoods *entity.Goods, e error) {\n goods := po.Goods{Name: name, Price: price, Stock: stock, Tag: tag, Created: time.Now(), Updated: time.Now()}\n\n _, e = createGoods(repo, &goods) //写库,createGoods是脚手架生成的函数。\n if e != nil {\n return\n }\n entityGoods = &entity.Goods{Goods: goods}\n repo.InjectBaseEntity(entityGoods)\n return\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"领域服务使用仓库"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"package domain\n\nimport (\n \"github.com/8treenet/freedom/example/fshop/domain/dto\"\n \"github.com/8treenet/freedom/example/fshop/adapter/repository\"\n \"github.com/8treenet/freedom/example/fshop/domain/aggregate\"\n \"github.com/8treenet/freedom/example/fshop/domain/entity\"\n \"github.com/8treenet/freedom/infra/transaction\"\n\n \"github.com/8treenet/freedom\"\n)\n\nfunc init() {\n freedom.Prepare(func(initiator freedom.Initiator) {\n //绑定创建领域服务函数到框架,框架会根据客户的使用做依赖倒置和依赖注入的处理。\n initiator.BindService(func() *Goods {\n //创建 Goods领域服务\n return &Goods{}\n })\n //控制器客户使用需要明确使用 InjectController\n initiator.InjectController(func(ctx freedom.Context) (service *Goods) {\n initiator.GetService(ctx, &service)\n return\n })\n })\n}\n\n// Goods 商品领域服务.\ntype Goods struct {\n Worker freedom.Worker //依赖注入请求运行时对象。\n GoodsRepo repository.Goods //依赖注入商品仓库\n}\n\n// New 创建商品\nfunc (g *Goods) New(name string, price int) (e error) {\n g.Worker.Logger().Info(\"创建商品\")\n _, e = g.GoodsRepo.New(name, entity.GoodsNoneTag, price, 100)\n return\n}\n\n// Items 分页商品列表\nfunc (g *Goods) Items(page, pagesize int, tag string) (items []dto.GoodsItemRes, e error) {\n entitys, e := g.GoodsRepo.FindsByPage(page, pagesize, tag)\n if e != nil {\n return\n }\n\n for i := 0; i < len(entitys); i++ {\n items = append(items, dto.GoodsItemRes{\n Id: entitys[i].Id,\n Name: entitys[i].Name,\n Price: entitys[i].Price,\n Stock: entitys[i].Stock,\n Tag: entitys[i].Tag,\n })\n }\n return\n}\n\n// AddStock 增加商品库存\nfunc (g *Goods) AddStock(goodsId, num int) (e error) {\n entity, e := g.GoodsRepo.Get(goodsId)\n if e != nil {\n return\n }\n\n entity.AddStock(num) //增加库存\n entity.DomainEvent(\"Goods.Stock\", entity) //发布增加商品库存的领域事件\n return g.GoodsRepo.Save(entity)\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"目录"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"golang领域模型-开篇"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"golang领域模型-六边形架构"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"golang领域模型-实体"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"golang领域模型-资源库"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"golang领域模型-依赖倒置"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"golang领域模型-聚合根"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"golang领域模型-CQRS"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"golang领域模型-领域事件"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"项目代码 "},{"type":"link","attrs":{"href":"https://github.com/8treenet/freedom/tree/master/example/fshop","title":null},"content":[{"type":"text","text":"https://github.com/8treenet/freedom/tree/master/example/fshop"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"PS:加我微信:stargaze111,拉你加入DDD交流群,一起切磋DDD与代码的艺术!"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}}]}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章