Golang領域模型-資源庫

{"type":"doc","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"前言:"},{"type":"text","text":" 作爲領域模型中最重要的環節之一的Repository,其通過對外暴露接口屏蔽了內部的複雜性,又有其"},{"type":"text","marks":[{"type":"strong"}],"text":"隱式寫時複製"},{"type":"text","text":"的巧妙代碼設計,完美的將DDD中的Repository的概念與代碼相結合!"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"Repository"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"資源庫通常標識一個存儲的區域,提供讀寫功能。通常我們將實體存放在資源庫中,之後通過該資源庫來獲取相同的實體,每一個實體都搭配一個資源庫。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如果你修改了某個實體,也需要通過資源庫去持久化。當然你也可以通過資源庫去刪除某一個實體。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"資源庫對外部是屏蔽了存儲細節的,資源庫內部去處理 cache、es、db。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/17/17b7439b88d0d4dc746965ca707e8470.png","alt":null,"title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":"center","origin":null},"content":[{"type":"text","marks":[{"type":"size","attrs":{"size":10}}],"text":"操作流程"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Repository解除了client的巨大負擔,使client只需與一個簡單的、易於理解的接口進行對話,並根據模型向這個接口提出它的請求。要實現所有這些功能需要大量複雜的技術基礎設施,但接口卻很簡單,而且在概念層次上與領域模型緊密聯繫在一起。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"隱式寫時複製"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"通常我們通過資源庫讀取一個實體後,再對這個實體進行修改。那麼這個修改後的持久化是需要知道實體的哪些屬性被修改,然後再對應的去持久化被修改的屬性。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"注意商品實體的changes"},{"type":"text","text":",商品被修改某個屬性,對應的Repository就持久化相應的修改。 這麼寫有什麼好處呢?如果不這麼做,那隻能在service裏調用orm指定更新列,但是這樣做的話,Repository的價值就完全被捨棄了!"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"可以說寫時複製是Repository和領域模型的橋樑!"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"//商品實體\ntype Goods struct {\n changes map[string]interface{} //被修改的屬性\n Name string //商品名稱\n Price int // 價格\n Stock int // 庫存\n}\n// SetPrice .\nfunc (obj *Goods) SetPrice(price int) {\n obj.Price = price\n obj.changes[\"price\"] = price //寫時複製\n}\n\n// SetStock .\nfunc (obj *Goods) SetStock(stock int) {\n obj.Stock = stock\n obj.changes[\"stock\"] = stock //寫時複製\n}\n\n//示例\nfunc main() {\n goodsEntity := GoodsRepository.Get(1) \n goodsEntity.SetPrice(1000)\n GoodsRepositorySave(goodsEntity) //GoodsRepository 會內部處理商品實體的changes\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"工廠和創建"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"創建商品實體需要唯一ID和已知的屬性名稱等,可以使用實體工廠去生成唯一ID和創建,在交給資源庫去持久化,這也是<>的作者推薦的方式,但這種方式更適合文檔型數據庫,唯一ID是Key和實體序列化是值。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"“底層技術可能會限制我們的建模選擇。例如,關係數據庫可能對複合對象結構的深度有實際的限制\"(領域驅動設計:軟件核心複雜性應對之道 Eric Evans)"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"但我們更多的使用的是關係型數據庫,這樣資源庫就需要創建的行爲。實體的唯一ID就是聚簇主鍵。一個實體或許是多張表組成,畢竟我們還要考慮垂直分表。我認爲DDD的範式和關係型數據庫範式,後者更重要。有時候我們還要爲Repository 實現一些統計select count(*)的功能。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"根據所使用的持久化技術和基礎設施不同,Repository的實現也將有很大的變化。理想的實現是向客戶隱藏所有內部工作細節(儘管不向客戶的開發人員隱藏這些細節),這樣不管數據是存儲在對象數據庫中,還是存儲在關係數據庫中,或是簡單地保持在內存中,客戶代碼都相同。Repository將會委託相應的基礎設施服務來完成工作。將存儲、檢索和查詢機制封裝起來是Repository實現的最基本的特性。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"實踐"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"link","attrs":{"href":"https://github.com/8treenet/freedom/tree/master/example/fshop/adapter/repository","title":null},"content":[{"type":"text","text":"https://github.com/8treenet/freedom/tree/master/example/fshop/adapter/repository"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"實體的緩存"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這個是緩存組件的接口,可以讀寫實體,實體的key 使用必須實現的Identity 方法。 - 一級緩存是基於請求的,首先會從一級緩存查找實體,生命週期是一個請求的開始和結束。 - 二級緩存是基於redis。 - 組件已經做了"},{"type":"text","marks":[{"type":"strong"}],"text":"冪等的防擊穿處理"},{"type":"text","text":"。 - SetSource設置持久化的回調函數,當一、二級緩存未命中,會讀取回調函數,並反寫一、二級緩存。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"// freedom.Entity\ntype Entity interface {\n DomainEvent(string, interface{},...map[string]string)\n Identity() string\n GetWorker() Worker\n SetProducer(string)\n Marshal() []byte\n}\n\n// infra.EntityCache\ntype EntityCache interface {\n //獲取實體\n GetEntity(freedom.Entity) error\n //刪除實體緩存\n Delete(result freedom.Entity, async ...bool) error\n //設置數據源\n SetSource(func(freedom.Entity) error) EntityCache\n //設置前綴\n SetPrefix(string) EntityCache\n //設置緩存時間,默認5分鐘\n SetExpiration(time.Duration) EntityCache\n //設置異步反寫緩存。默認關閉,緩存未命中讀取數據源後的異步反寫緩存\n SetAsyncWrite(bool) EntityCache\n //設置防擊穿,默認開啓\n SetSingleFlight(bool) EntityCache\n //關閉二級緩存. 關閉後只有一級緩存生效\n CloseRedis() EntityCache\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"以下實現了一個商品的資源庫"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"package repository\n\nimport (\n \"time\"\n\n \"github.com/8treenet/freedom/infra/store\"\n\n \"github.com/8treenet/freedom/example/fshop/domain/po\"\n \"github.com/8treenet/freedom/example/fshop/domain/entity\"\n\n \"github.com/8treenet/freedom\"\n)\n\nfunc init() {\n freedom.Prepare(func(initiator freedom.Initiator) {\n //綁定創建資源庫函數到框架,框架會根據客戶的使用做依賴倒置和依賴注入的處理。\n initiator.BindRepository(func() *Goods {\n //創建 Goods資源庫\n return &Goods{}\n })\n })\n}\n// Goods .\ntype Goods struct {\n freedom.Repository //資源庫必須繼承,這樣是爲了約束 db、redis、http等的訪問\n Cache store.EntityCache //依賴注入實體緩存組件\n}\n\n// BeginRequest\nfunc (repo *Goods) BeginRequest(worker freedom.Worker) {\n repo.Repository.BeginRequest(worker)\n\n //設置緩存的持久化數據源,旁路緩存模型,如果緩存未有數據,將回調該函數。\n repo.Cache.SetSource(func(result freedom.Entity) error {\n\nreturn findGoods(repo, result) \n })\n //緩存30秒, 不設置默認5分鐘\n repo.Cache.SetExpiration(30 * time.Second)\n //設置緩存前綴\n repo.Cache.SetPrefix(\"freedom\")\n}\n\n// Get 通過id 獲取商品實體.\nfunc (repo *Goods) Get(id int) (goodsEntity *entity.Goods, e error) {\n goodsEntity = &entity.Goods{}\n goodsEntity.Id = id\n //注入基礎Entity 包含運行時和領域事件的producer\n repo.InjectBaseEntity(goodsEntity)\n\n //讀取緩存, Identity() 會返回 id,緩存會使用它當key\n return goodsEntity, repo.Cache.GetEntity(goodsEntity)\n}\n\n// Save 持久化實體.\nfunc (repo *Goods) Save(entity *entity.Goods) error {\n _, e := saveGoods(repo, entity) //寫庫,saveGoods是腳手架生成的函數,會做寫時複製的處理。\n //清空緩存\n repo.Cache.Delete(entity)\n return e\n}\n\nfunc (repo *Goods) FindsByPage(page, pageSize int, tag string) (entitys []*entity.Goods, e error) {\n build := repo.NewORMDescBuilder(\"id\").NewPageBuilder(page, pageSize) //創建分頁器\n e = findGoodsList(repo, po.Goods{Tag: tag}, &entitys, build)\n if e != nil {\n return\n }\n //注入基礎Entity 包含運行時和領域事件的producer\n repo.InjectBaseEntitys(entitys)\n return\n}\n\nfunc (repo *Goods) New(name, tag string, price, stock int) (entityGoods *entity.Goods, e error) {\n goods := po.Goods{Name: name, Price: price, Stock: stock, Tag: tag, Created: time.Now(), Updated: time.Now()}\n\n _, e = createGoods(repo, &goods) //寫庫,createGoods是腳手架生成的函數。\n if e != nil {\n return\n }\n entityGoods = &entity.Goods{Goods: goods}\n repo.InjectBaseEntity(entityGoods)\n return\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"領域服務使用倉庫"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"package domain\n\nimport (\n \"github.com/8treenet/freedom/example/fshop/domain/dto\"\n \"github.com/8treenet/freedom/example/fshop/adapter/repository\"\n \"github.com/8treenet/freedom/example/fshop/domain/aggregate\"\n \"github.com/8treenet/freedom/example/fshop/domain/entity\"\n \"github.com/8treenet/freedom/infra/transaction\"\n\n \"github.com/8treenet/freedom\"\n)\n\nfunc init() {\n freedom.Prepare(func(initiator freedom.Initiator) {\n //綁定創建領域服務函數到框架,框架會根據客戶的使用做依賴倒置和依賴注入的處理。\n initiator.BindService(func() *Goods {\n //創建 Goods領域服務\n return &Goods{}\n })\n //控制器客戶使用需要明確使用 InjectController\n initiator.InjectController(func(ctx freedom.Context) (service *Goods) {\n initiator.GetService(ctx, &service)\n return\n })\n })\n}\n\n// Goods 商品領域服務.\ntype Goods struct {\n Worker freedom.Worker //依賴注入請求運行時對象。\n GoodsRepo repository.Goods //依賴注入商品倉庫\n}\n\n// New 創建商品\nfunc (g *Goods) New(name string, price int) (e error) {\n g.Worker.Logger().Info(\"創建商品\")\n _, e = g.GoodsRepo.New(name, entity.GoodsNoneTag, price, 100)\n return\n}\n\n// Items 分頁商品列表\nfunc (g *Goods) Items(page, pagesize int, tag string) (items []dto.GoodsItemRes, e error) {\n entitys, e := g.GoodsRepo.FindsByPage(page, pagesize, tag)\n if e != nil {\n return\n }\n\n for i := 0; i < len(entitys); i++ {\n items = append(items, dto.GoodsItemRes{\n Id: entitys[i].Id,\n Name: entitys[i].Name,\n Price: entitys[i].Price,\n Stock: entitys[i].Stock,\n Tag: entitys[i].Tag,\n })\n }\n return\n}\n\n// AddStock 增加商品庫存\nfunc (g *Goods) AddStock(goodsId, num int) (e error) {\n entity, e := g.GoodsRepo.Get(goodsId)\n if e != nil {\n return\n }\n\n entity.AddStock(num) //增加庫存\n entity.DomainEvent(\"Goods.Stock\", entity) //發佈增加商品庫存的領域事件\n return g.GoodsRepo.Save(entity)\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"目錄"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"golang領域模型-開篇"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"golang領域模型-六邊形架構"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"golang領域模型-實體"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"golang領域模型-資源庫"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"golang領域模型-依賴倒置"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"golang領域模型-聚合根"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"golang領域模型-CQRS"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"golang領域模型-領域事件"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"項目代碼 "},{"type":"link","attrs":{"href":"https://github.com/8treenet/freedom/tree/master/example/fshop","title":null},"content":[{"type":"text","text":"https://github.com/8treenet/freedom/tree/master/example/fshop"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"PS:加我微信:stargaze111,拉你加入DDD交流羣,一起切磋DDD與代碼的藝術!"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}}]}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章