前言: 作為領域模型中最重要的環(huán)節(jié)之一的Repository
,其通過對外暴露接口屏蔽了內(nèi)部的復雜性臭蚁,又有其隱式寫時復制的巧妙代碼設計瞧捌,完美的將DDD
中的Repository
的概念與代碼相結合!
Repository
資源庫通常標識一個存儲的區(qū)域义黎,提供讀寫功能禾进。通常我們將實體存放在資源庫中,之后通過該資源庫來獲取相同的實體廉涕,每一個實體都搭配一個資源庫泻云。
如果你修改了某個實體,也需要通過資源庫去持久化狐蜕。當然你也可以通過資源庫去刪除某一個實體宠纯。
資源庫對外部是屏蔽了存儲細節(jié)的,資源庫內(nèi)部去處理 cache
层释、es
婆瓜、db
。
Repository
解除了client
的巨大負擔贡羔,使client
只需與一個簡單的廉白、易于理解的接口進行對話,并根據(jù)模型向這個接口提出它的請求乖寒。要實現(xiàn)所有這些功能需要大量復雜的技術基礎設施蒙秒,但接口卻很簡單,而且在概念層次上與領域模型緊密聯(lián)系在一起宵统。
隱式寫時復制
通常我們通過資源庫讀取一個實體后晕讲,再對這個實體進行修改覆获。那么這個修改后的持久化是需要知道實體的哪些屬性被修改,然后再對應的去持久化被修改的屬性瓢省。
注意商品實體的changes
弄息,商品被修改某個屬性,對應的Repository
就持久化相應的修改勤婚。
這么寫有什么好處呢摹量?如果不這么做,那只能在service里調(diào)用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 會內(nèi)部處理商品實體的changes
}
工廠和創(chuàng)建
創(chuàng)建商品實體需要唯一ID
和已知的屬性名稱等睦尽,可以使用實體工廠去生成唯一ID
和創(chuàng)建,在交給資源庫去持久化型雳,這也是<<實現(xiàn)領域驅(qū)動設計>>的作者推薦的方式当凡,但這種方式更適合文檔型數(shù)據(jù)庫,唯一ID
是Key
和實體序列化是值纠俭。
“底層技術可能會限制我們的建模選擇沿量。例如,關系數(shù)據(jù)庫可能對復合對象結構的深度有實際的限制"(領域驅(qū)動設計:軟件核心復雜性應對之道 Eric Evans
)
但我們更多的使用的是關系型數(shù)據(jù)庫冤荆,這樣資源庫就需要創(chuàng)建的行為朴则。實體的唯一ID
就是聚簇主鍵。一個實體或許是多張表組成钓简,畢竟我們還要考慮垂直分表佛掖。我認為DDD
的范式和關系型數(shù)據(jù)庫范式,后者更重要涌庭。有時候我們還要為Repository
實現(xiàn)一些統(tǒng)計select count(*)
的功能芥被。
根據(jù)所使用的持久化技術和基礎設施不同,Repository
的實現(xiàn)也將有很大的變化坐榆。理想的實現(xiàn)是向客戶隱藏所有內(nèi)部工作細節(jié)(盡管不向客戶的開發(fā)人員隱藏這些細節(jié))拴魄,這樣不管數(shù)據(jù)是存儲在對象數(shù)據(jù)庫中,還是存儲在關系數(shù)據(jù)庫中席镀,或是簡單地保持在內(nèi)存中匹中,客戶代碼都相同。Repository
將會委托相應的基礎設施服務來完成工作豪诲。將存儲顶捷、檢索和查詢機制封裝起來是Repository
實現(xiàn)的最基本的特性。
實踐
https://github.com/8treenet/freedom/tree/master/example/fshop/adapter/repository
實體的緩存
這個是緩存組件的接口屎篱,可以讀寫實體服赎,實體的key
使用必須實現(xiàn)的Identity
方法葵蒂。
- 一級緩存是基于請求的,首先會從一級緩存查找實體重虑,生命周期是一個請求的開始和結束践付。
- 二級緩存是基于
redis
。 - 組件已經(jīng)做了冪等的防擊穿處理缺厉。
-
SetSource
設置持久化的回調(diào)函數(shù)永高,當一、二級緩存未命中提针,會讀取回調(diào)函數(shù)命爬,并反寫一、二級緩存辐脖。
// freedom.Entity
type Entity interface {
DomainEvent(string, interface{},...map[string]string)
Identity() string
GetWorker() Worker
SetProducer(string)
Marshal() []byte
}
// infra.EntityCache
type EntityCache interface {
//獲取實體
GetEntity(freedom.Entity) error
//刪除實體緩存
Delete(result freedom.Entity, async ...bool) error
//設置數(shù)據(jù)源
SetSource(func(freedom.Entity) error) EntityCache
//設置前綴
SetPrefix(string) EntityCache
//設置緩存時間饲宛,默認5分鐘
SetExpiration(time.Duration) EntityCache
//設置異步反寫緩存。默認關閉揖曾,緩存未命中讀取數(shù)據(jù)源后的異步反寫緩存
SetAsyncWrite(bool) EntityCache
//設置防擊穿落萎,默認開啟
SetSingleFlight(bool) EntityCache
//關閉二級緩存. 關閉后只有一級緩存生效
CloseRedis() EntityCache
}
以下實現(xiàn)了一個商品的資源庫
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) {
//綁定創(chuàng)建資源庫函數(shù)到框架亥啦,框架會根據(jù)客戶的使用做依賴倒置和依賴注入的處理炭剪。
initiator.BindRepository(func() *Goods {
//創(chuàng)建 Goods資源庫
return &Goods{}
})
})
}
// Goods .
type Goods struct {
freedom.Repository //資源庫必須繼承,這樣是為了約束 db翔脱、redis奴拦、http等的訪問
Cache store.EntityCache //依賴注入實體緩存組件
}
// BeginRequest
func (repo *Goods) BeginRequest(worker freedom.Worker) {
repo.Repository.BeginRequest(worker)
//設置緩存的持久化數(shù)據(jù)源,旁路緩存模型,如果緩存未有數(shù)據(jù)届吁,將回調(diào)該函數(shù)错妖。
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是腳手架生成的函數(shù),會做寫時復制的處理亮蛔。
//清空緩存
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) //創(chuàng)建分頁器
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是腳手架生成的函數(shù)。
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) {
//綁定創(chuàng)建領域服務函數(shù)到框架究流,框架會根據(jù)客戶的使用做依賴倒置和依賴注入的處理辣吃。
initiator.BindService(func() *Goods {
//創(chuàng)建 Goods領域服務
return &Goods{}
})
//控制器客戶使用需要明確使用 InjectController
initiator.InjectController(func(ctx freedom.Context) (service *Goods) {
initiator.GetService(ctx, &service)
return
})
})
}
// Goods 商品領域服務.
type Goods struct {
Worker freedom.Worker //依賴注入請求運行時對象。
GoodsRepo repository.Goods //依賴注入商品倉庫
}
// New 創(chuàng)建商品
func (g *Goods) New(name string, price int) (e error) {
g.Worker.Logger().Info("創(chuàng)建商品")
_, 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) //發(fā)布增加商品庫存的領域事件
return g.GoodsRepo.Save(entity)
}
目錄
- golang領域模型-開篇
- golang領域模型-六邊形架構
- golang領域模型-實體
- golang領域模型-資源庫
- golang領域模型-依賴倒置
- golang領域模型-聚合根
- golang領域模型-CQRS
- golang領域模型-領域事件
項目代碼 https://github.com/8treenet/freedom/tree/master/example/fshop
PS:關注公眾號《從菜鳥到大佬》芬探,發(fā)送消息“加群”或“領域模型”神得,加入DDD交流群,一起切磋DDD與代碼的藝術偷仿!