為 Gopher 打造 DDD 系列:領(lǐng)域模型-資源庫(kù)
前言: 作為領(lǐng)域模型中最重要的環(huán)節(jié)之一的Repository,其通過對(duì)外暴露接口屏蔽了內(nèi)部的復(fù)雜性,又有其隱式寫時(shí)復(fù)制的巧妙代碼設(shè)計(jì),完美的將DDD中的Repository的概念與代碼相結(jié)合!
Repository
資源庫(kù)通常標(biāo)識(shí)一個(gè)存儲(chǔ)的區(qū)域,提供讀寫功能。通常我們將實(shí)體存放在資源庫(kù)中,之后通過該資源庫(kù)來獲取相同的實(shí)體,每一個(gè)實(shí)體都搭配一個(gè)資源庫(kù)。
如果你修改了某個(gè)實(shí)體,也需要通過資源庫(kù)去持久化。當(dāng)然你也可以通過資源庫(kù)去刪除某一個(gè)實(shí)體。
資源庫(kù)對(duì)外部是屏蔽了存儲(chǔ)細(xì)節(jié)的,資源庫(kù)內(nèi)部去處理 cache、es、db。
數(shù)據(jù)操作流程
Repository解除了client的巨大負(fù)擔(dān),使client只需與一個(gè)簡(jiǎn)單的、易于理解的接口進(jìn)行對(duì)話,并根據(jù)模型向這個(gè)接口提出它的請(qǐng)求。要實(shí)現(xiàn)所有這些功能需要大量復(fù)雜的技術(shù)基礎(chǔ)設(shè)施,但接口卻很簡(jiǎn)單,而且在概念層次上與領(lǐng)域模型緊密聯(lián)系在一起。
隱式寫時(shí)復(fù)制
通常我們通過資源庫(kù)讀取一個(gè)實(shí)體后,再對(duì)這個(gè)實(shí)體進(jìn)行修改。那么這個(gè)修改后的持久化是需要知道實(shí)體的哪些屬性被修改,然后再對(duì)應(yīng)的去持久化被修改的屬性。
注意商品實(shí)體的changes,商品被修改某個(gè)屬性,對(duì)應(yīng)的Repository就持久化相應(yīng)的修改。這么寫有什么好處呢?如果不這么做,那只能在service里調(diào)用orm指定更新列,但是這樣做的話,Repository的價(jià)值就完全被舍棄了!
可以說寫時(shí)復(fù)制是Repository和領(lǐng)域模型的橋梁!
//商品實(shí)體type Goods struct {changes map[string]interface{} //被修改的屬性Name string//商品名稱Price int// 價(jià)格Stock int// 庫(kù)存}// SetPrice .func (obj *Goods) SetPrice(price int) {obj.Price = priceobj.changes["price"] = price //寫時(shí)復(fù)制}// SetStock .func (obj *Goods) SetStock(stock int) {obj.Stock = stockobj.changes["stock"] = stock //寫時(shí)復(fù)制}//示例func main() {goodsEntity := GoodsRepository.Get(1)goodsEntity.SetPrice(1000)GoodsRepositorySave(goodsEntity) //GoodsRepository 會(huì)內(nèi)部處理商品實(shí)體的changes}工廠和創(chuàng)建
創(chuàng)建商品實(shí)體需要唯一ID和已知的屬性名稱等,可以使用實(shí)體工廠去生成唯一ID和創(chuàng)建,在交給資源庫(kù)去持久化,這也是<<實(shí)現(xiàn)領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)>>的作者推薦的方式,但這種方式更適合文檔型數(shù)據(jù)庫(kù),唯一ID是Key和實(shí)體序列化是值。
“底層技術(shù)可能會(huì)限制我們的建模選擇。例如,關(guān)系數(shù)據(jù)庫(kù)可能對(duì)復(fù)合對(duì)象結(jié)構(gòu)的深度有實(shí)際的限制"(領(lǐng)域驅(qū)動(dòng)設(shè)計(jì):軟件核心復(fù)雜性應(yīng)對(duì)之道 Eric Evans)
但我們更多的使用的是關(guān)系型數(shù)據(jù)庫(kù),這樣資源庫(kù)就需要?jiǎng)?chuàng)建的行為。實(shí)體的唯一ID就是聚簇主鍵。一個(gè)實(shí)體或許是多張表組成,畢竟我們還要考慮垂直分表。我認(rèn)為DDD的范式和關(guān)系型數(shù)據(jù)庫(kù)范式,后者更重要。有時(shí)候我們還要為Repository 實(shí)現(xiàn)一些統(tǒng)計(jì)select count(*)的功能。
根據(jù)所使用的持久化技術(shù)和基礎(chǔ)設(shè)施不同,Repository的實(shí)現(xiàn)也將有很大的變化。理想的實(shí)現(xiàn)是向客戶隱藏所有內(nèi)部工作細(xì)節(jié)(盡管不向客戶的開發(fā)人員隱藏這些細(xì)節(jié)),這樣不管數(shù)據(jù)是存儲(chǔ)在對(duì)象數(shù)據(jù)庫(kù)中,還是存儲(chǔ)在關(guān)系數(shù)據(jù)庫(kù)中,或是簡(jiǎn)單地保持在內(nèi)存中,客戶代碼都相同。Repository將會(huì)委托相應(yīng)的基礎(chǔ)設(shè)施服務(wù)來完成工作。將存儲(chǔ)、檢索和查詢機(jī)制封裝起來是Repository實(shí)現(xiàn)的最基本的特性。
實(shí)踐
https://github.com/8treenet/freedom/tree/master/example/fshop/adapter/repository
實(shí)體的緩存
這個(gè)是緩存組件的接口,可以讀寫實(shí)體,實(shí)體的key 使用必須實(shí)現(xiàn)的Identity 方法。
一級(jí)緩存是基于請(qǐng)求的,首先會(huì)從一級(jí)緩存查找實(shí)體,生命周期是一個(gè)請(qǐng)求的開始和結(jié)束。 二級(jí)緩存是基于 redis。組件已經(jīng)做了冪等的防擊穿處理。 SetSource設(shè)置持久化的回調(diào)函數(shù),當(dāng)一、二級(jí)緩存未命中,會(huì)讀取回調(diào)函數(shù),并反寫一、二級(jí)緩存。
// freedom.Entitytype Entity interface {DomainEvent(string, interface{},...map[string]string)Identity() stringGetWorker() WorkerSetProducer(string)Marshal() []byte}// infra.EntityCachetype EntityCache interface {//獲取實(shí)體GetEntity(freedom.Entity) error//刪除實(shí)體緩存Delete(result freedom.Entity, async ...bool) error//設(shè)置數(shù)據(jù)源SetSource(func(freedom.Entity) error) EntityCache//設(shè)置前綴SetPrefix(string) EntityCache//設(shè)置緩存時(shí)間,默認(rèn)5分鐘SetExpiration(time.Duration) EntityCache//設(shè)置異步反寫緩存。默認(rèn)關(guān)閉,緩存未命中讀取數(shù)據(jù)源后的異步反寫緩存SetAsyncWrite(bool) EntityCache//設(shè)置防擊穿,默認(rèn)開啟SetSingleFlight(bool) EntityCache//關(guān)閉二級(jí)緩存. 關(guān)閉后只有一級(jí)緩存生效CloseRedis() EntityCache}以下實(shí)現(xiàn)了一個(gè)商品的資源庫(kù)
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 //資源庫(kù)必須繼承,這樣是為了約束 db、redis、http等的訪問Cache store.EntityCache //依賴注入實(shí)體緩存組件}// BeginRequestfunc (repo *Goods) BeginRequest(worker freedom.Worker) {repo.Repository.BeginRequest(worker)//設(shè)置緩存的持久化數(shù)據(jù)源,旁路緩存模型,如果緩存未有數(shù)據(jù),將回調(diào)該函數(shù)。repo.Cache.SetSource(func(result freedom.Entity) error {return findGoods(repo, result)})//緩存30秒, 不設(shè)置默認(rèn)5分鐘repo.Cache.SetExpiration(30 * time.Second)//設(shè)置緩存前綴repo.Cache.SetPrefix("freedom")}// Get 通過id 獲取商品實(shí)體.func (repo *Goods) Get(id int) (goodsEntity *entity.Goods, e error) {goodsEntity = &entity.Goods{}goodsEntity.Id = id//注入基礎(chǔ)Entity 包含運(yùn)行時(shí)和領(lǐng)域事件的producerrepo.InjectBaseEntity(goodsEntity)//讀取緩存, Identity() 會(huì)返回 id,緩存會(huì)使用它當(dāng)keyreturn goodsEntity, repo.Cache.GetEntity(goodsEntity)}// Save 持久化實(shí)體.func (repo *Goods) Save(entity *entity.Goods) error {_, e := saveGoods(repo, entity) //寫庫(kù),saveGoods是腳手架生成的函數(shù),會(huì)做寫時(shí)復(fù)制的處理。//清空緩存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)建分頁(yè)器e = findGoodsList(repo, po.Goods{Tag: tag}, &entitys, build)if e != nil {return}//注入基礎(chǔ)Entity 包含運(yùn)行時(shí)和領(lǐng)域事件的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) //寫庫(kù),createGoods是腳手架生成的函數(shù)。if e != nil {return}entityGoods = &entity.Goods{Goods: goods}repo.InjectBaseEntity(entityGoods)return}領(lǐng)域服務(wù)使用倉(cāng)庫(kù)
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 商品領(lǐng)域服務(wù).type Goods struct {Worker freedom.Worker //依賴注入請(qǐng)求運(yùn)行時(shí)對(duì)象。GoodsRepo repository.Goods //依賴注入商品倉(cāng)庫(kù)}// 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 分頁(yè)商品列表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 增加商品庫(kù)存func (g *Goods) AddStock(goodsId, num int) (e error) {entity, e := g.GoodsRepo.Get(goodsId)if e != nil {return}entity.AddStock(num) //增加庫(kù)存entity.DomainEvent("Goods.Stock", entity) //發(fā)布增加商品庫(kù)存的領(lǐng)域事件return g.GoodsRepo.Save(entity)}
項(xiàng)目代碼 https://github.com/8treenet/freedom/tree/master/example/fshop
推薦閱讀
站長(zhǎng) polarisxu
自己的原創(chuàng)文章
不限于 Go 技術(shù)
職場(chǎng)和創(chuàng)業(yè)經(jīng)驗(yàn)
Go語(yǔ)言中文網(wǎng)
每天為你
分享 Go 知識(shí)
Go愛好者值得關(guān)注
