Golang領域模型-資源庫

前言: 作為領域模型中最重要的環(huán)節(jié)之一的Repository,其通過對外暴露接口屏蔽了內部的復雜性,又有其隱式寫時復制的巧妙代碼設計,完美的將DDD中的Repository的概念與代碼相結合!

Repository

資源庫通常標識一個存儲的區(qū)域,提供讀寫功能。通常我們將實體存放在資源庫中,之后通過該資源庫來獲取相同的實體,每一個實體都搭配一個資源庫。

如果你修改了某個實體,也需要通過資源庫去持久化。當然你也可以通過資源庫去刪除某一個實體。

資源庫對外部是屏蔽了存儲細節(jié)的,資源庫內部去處理 cachees、db。

操作流程

Repository解除了client的巨大負擔,使client只需與一個簡單的、易于理解的接口進行對話,并根據(jù)模型向這個接口提出它的請求。要實現(xiàn)所有這些功能需要大量復雜的技術基礎設施,但接口卻很簡單,而且在概念層次上與領域模型緊密聯(lián)系在一起。

隱式寫時復制

通常我們通過資源庫讀取一個實體后,再對這個實體進行修改。那么這個修改后的持久化是需要知道實體的哪些屬性被修改,然后再對應的去持久化被修改的屬性。

注意商品實體的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 = 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 會內部處理商品實體的changes
}

工廠和創(chuàng)建

創(chuàng)建商品實體需要唯一ID和已知的屬性名稱等,可以使用實體工廠去生成唯一ID和創(chuàng)建,在交給資源庫去持久化,這也是<<實現(xiàn)領域驅動設計>>的作者推薦的方式,但這種方式更適合文檔型數(shù)據(jù)庫,唯一IDKey和實體序列化是值。

“底層技術可能會限制我們的建模選擇。例如,關系數(shù)據(jù)庫可能對復合對象結構的深度有實際的限制"(領域驅動設計:軟件核心復雜性應對之道 Eric Evans)

但我們更多的使用的是關系型數(shù)據(jù)庫,這樣資源庫就需要創(chuàng)建的行為。實體的唯一ID就是聚簇主鍵。一個實體或許是多張表組成,畢竟我們還要考慮垂直分表。我認為DDD的范式和關系型數(shù)據(jù)庫范式,后者更重要。有時候我們還要為Repository 實現(xiàn)一些統(tǒng)計select count(*)的功能。

根據(jù)所使用的持久化技術和基礎設施不同,Repository的實現(xiàn)也將有很大的變化。理想的實現(xiàn)是向客戶隱藏所有內部工作細節(jié)(盡管不向客戶的開發(fā)人員隱藏這些細節(jié)),這樣不管數(shù)據(jù)是存儲在對象數(shù)據(jù)庫中,還是存儲在關系數(shù)據(jù)庫中,或是簡單地保持在內存中,客戶代碼都相同。Repository將會委托相應的基礎設施服務來完成工作。將存儲、檢索和查詢機制封裝起來是Repository實現(xiàn)的最基本的特性。

實踐

https://github.com/8treenet/freedom/tree/master/example/fshop/adapter/repository

實體的緩存

這個是緩存組件的接口,可以讀寫實體,實體的key 使用必須實現(xiàn)的Identity 方法。

  • 一級緩存是基于請求的,首先會從一級緩存查找實體,生命周期是一個請求的開始和結束。
  • 二級緩存是基于redis
  • 組件已經做了冪等的防擊穿處理。
  • SetSource設置持久化的回調函數(shù),當一、二級緩存未命中,會讀取回調函數(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ù),將回調該函數(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與代碼的藝術!

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

友情鏈接更多精彩內容