Golang領域模型-資源庫

前言: 作為領域模型中最重要的環(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ù)庫,唯一IDKey和實體序列化是值纠俭。

“底層技術可能會限制我們的建模選擇沿量。例如,關系數(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與代碼的藝術偷仿!

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末哩簿,一起剝皮案震驚了整個濱河市宵蕉,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌卡骂,老刑警劉巖国裳,帶你破解...
    沈念sama閱讀 212,454評論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異全跨,居然都是意外死亡缝左,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,553評論 3 385
  • 文/潘曉璐 我一進店門浓若,熙熙樓的掌柜王于貴愁眉苦臉地迎上來渺杉,“玉大人,你說我怎么就攤上這事挪钓∈窃剑” “怎么了?”我有些...
    開封第一講書人閱讀 157,921評論 0 348
  • 文/不壞的土叔 我叫張陵碌上,是天一觀的道長倚评。 經(jīng)常有香客問我,道長馏予,這世上最難降的妖魔是什么天梧? 我笑而不...
    開封第一講書人閱讀 56,648評論 1 284
  • 正文 為了忘掉前任,我火速辦了婚禮霞丧,結果婚禮上呢岗,老公的妹妹穿的比我還像新娘。我一直安慰自己蛹尝,他們只是感情好后豫,可當我...
    茶點故事閱讀 65,770評論 6 386
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著突那,像睡著了一般挫酿。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上愕难,一...
    開封第一講書人閱讀 49,950評論 1 291
  • 那天早龟,我揣著相機與錄音,去河邊找鬼务漩。 笑死拄衰,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的饵骨。 我是一名探鬼主播翘悉,決...
    沈念sama閱讀 39,090評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼居触!你這毒婦竟也來了妖混?” 一聲冷哼從身側(cè)響起老赤,我...
    開封第一講書人閱讀 37,817評論 0 268
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎制市,沒想到半個月后抬旺,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,275評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡祥楣,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,592評論 2 327
  • 正文 我和宋清朗相戀三年开财,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片误褪。...
    茶點故事閱讀 38,724評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡责鳍,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出兽间,到底是詐尸還是另有隱情历葛,我是刑警寧澤,帶...
    沈念sama閱讀 34,409評論 4 333
  • 正文 年R本政府宣布嘀略,位于F島的核電站恤溶,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏帜羊。R本人自食惡果不足惜咒程,卻給世界環(huán)境...
    茶點故事閱讀 40,052評論 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望逮壁。 院中可真熱鬧孵坚,春花似錦粮宛、人聲如沸窥淆。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,815評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽忧饭。三九已至,卻和暖如春筷畦,著一層夾襖步出監(jiān)牢的瞬間词裤,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,043評論 1 266
  • 我被黑心中介騙來泰國打工鳖宾, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留吼砂,地道東北人。 一個月前我還...
    沈念sama閱讀 46,503評論 2 361
  • 正文 我出身青樓鼎文,卻偏偏與公主長得像渔肩,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子拇惋,可洞房花燭夜當晚...
    茶點故事閱讀 43,627評論 2 350