商品领域ddd_为 Gopher 打造 DDD 系列:领域模型-资源库
前言: 作為領域模型中最重要的環節之一的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 = priceobj.changes["price"] = price //寫時復制}// SetStock .func (obj *Goods) SetStock(stock int) {obj.Stock = stockobj.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() stringGetWorker() WorkerSetProducer(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 包含運行時和領域事件的producerrepo.InjectBaseEntity(goodsEntity)//讀取緩存, Identity() 會返回 id,緩存會使用它當keyreturn 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 包含運行時和領域事件的producerrepo.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
總結
以上是生活随笔為你收集整理的商品领域ddd_为 Gopher 打造 DDD 系列:领域模型-资源库的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Android日志[基础篇]Androi
- 下一篇: python乘法表代码注释_Python