為 Gopher 打造 DDD 系列:領域模型-實體
前言: 實體具有業(yè)務屬性、業(yè)務邏輯和業(yè)務行為,是是實實在在的業(yè)務對象。在事件風暴中,我們可以根據(jù)命令、操作與事件將業(yè)務上緊密結合在一起的多個實體與值對象進行聚合形成聚合根。
實體是什么
雖然數(shù)據(jù)庫的設計占據(jù)了主導地位(這個是沒錯的),但開發(fā)者也不應該只關注數(shù)據(jù),而且要關注模型。
數(shù)據(jù)+行為= 模型,實體就是含有領域概念的模型。它是一個唯一的東西,在相當長的時間里數(shù)據(jù)狀態(tài)在持續(xù)地變化,并且一定有唯一鍵,這區(qū)別于值對象。
注意的是如果非要用表結構里的一條含有主鍵的數(shù)據(jù)去理解實體也是可以的,但不少情況下可能是有多個表或者k/v數(shù)據(jù)來組成的一個實體。
實體、值對象與數(shù)據(jù)模型示例

實體是可變的,是變性,每個用戶實體都有自己的唯一性,我們用id來進行區(qū)分。值對象是不變的,是共性,實體都有相同的值對象,例如國家等信息。我們以此區(qū)分好實體與值對象。
為什么使用實體
如果開發(fā)者設計系統(tǒng)時,并沒有建模而是直接開始設計表結構和它的CRUD,其實這也算是建模的一種,但這種方式僅僅能應對簡單的模型。這樣的操作當更復雜的業(yè)務和更復雜的模型出現(xiàn)后是駕馭不了的。假設我們做10平米的臥室設計,那么簡單的量一下桌椅床就ok。但如果我們設計一個200平米的化學實驗室的時候,這個簡單的擺放可能搞不好會導致爆炸吧~
實體的數(shù)據(jù)與行為
唯一標識
在實體的設計早期,我們將關注點都放在了實體的身份唯一性、屬性、行為上。同時還應該關注對實體的查詢和創(chuàng)建,我們首先要考慮實體的本質特征,特別是實體的唯一標識符,它是一個關系節(jié)點。比如用戶這個實體username是不是唯一標識,如果不是唯一,是不是可以通過username去查找。
實踐
https://github.com/8treenet/freedom/tree/master/example/fshop/domain/entity
所有的 entity都必須繼承freedom.Entity接口, 這里為實體注入了領域事件和運行時的Worker對象。所有的 entity都必須重寫Identity() string方法。實體可以選擇的繼承 PO或者DTO,PO是通過腳手架生成的表模型屬性和Get/Set方法。
發(fā)布領域事件
type Entity interface {//發(fā)布領域事件DomainEvent(string,interface{},...map[string]string)//唯一IDIdentity() string//獲取請求運行時對象GetWorker() WorkerSetProducer(string)Marshal() []byte}

商品實體
package entityimport ("errors""strconv""github.com/8treenet/freedom""github.com/8treenet/freedom/example/fshop/domain/po")const (//熱銷GoodsHotTag = "HOT"//新品GoodsNewTag = "NEW"GoodsNoneTag = "NONE")// 商品實體type Goods struct {freedom.Entity //繼承實體基類接口po.Goods //繼承持久化的商品模型,包含了商品的列和屬性方法}// Identity 唯一func (g *Goods) Identity() string {return strconv.Itoa(g.Id)}// CutStock 扣庫存func (g *Goods) CutStock(num int) error {if num > g.Stock {return errors.New("庫存不足")}g.AddStock(-num) //po對象的方法,增加庫存return nil}// MarkedTag 為商品打tagfunc (g *Goods) MarkedTag(tag string) error {if tag != GoodsHotTag && tag != GoodsNewTag && tag != GoodsNoneTag {return errors.New("Tag doesn't exist")}g.SetTag(tag) //po對象的方法,設置tagreturn nil}用戶實體
package entityimport ("errors""strconv""github.com/8treenet/freedom""github.com/8treenet/freedom/example/fshop/domain/po")// 用戶實體type User struct {freedom.Entity //繼承實體基類接口po.User //繼承持久化的用戶模型,包含了用戶的列和屬性方法}// Identity 唯一func (u *User) Identity() string {return strconv.Itoa(u.Id)}// ChangePassword 修改密碼func (u *User) ChangePassword(newPassword, oldPassword string) error {//判斷舊密碼是否正確if u.Password != oldPassword {return errors.New("Password error")}u.SetPassword(newPassword) //po對象的方法,可以設置密碼returnnil}
訂單實體
package entityimport ("github.com/8treenet/freedom""github.com/8treenet/freedom/example/fshop/domain/po")const (OrderStatusPAID = "PAID" //付款OrderStatusNonPayment = "NON_PAYMENT" //未付款OrderStatusShipment = "SHIPMENT" //發(fā)貨)// 訂單實體type Order struct {freedom.Entity //繼承實體基類接口po.Order //繼承持久化的訂單模型,包含了訂單的列和屬性方法Details []*po.OrderDetail //定義訂單商品詳情成員變量,一個訂單包含多個商品}// Identity 唯一func (o *Order) Identity() string {return o.OrderNo}// AddOrderDetal 增加訂單詳情func (o *Order) AddOrderDetal(detal *po.OrderDetail) {o.Details = append(o.Details, detal) //增加訂單詳情,repository會做持久化處理}// Pay 付款func (o *Order) Pay() {o.SetStatus(OrderStatusPAID) //po對象的方法,設置狀態(tài)}// Shipment 發(fā)貨func (o *Order) Shipment() {o.SetStatus(OrderStatusShipment) //po對象的方法,設置狀態(tài)}// IsPay 是否支付func (o *Order) IsPay() bool {//判斷是否付款if o.Status != OrderStatusPAID {return false}return true}
目錄
golang領域模型-開篇 golang領域模型-六邊形架構 golang領域模型-實體 golang領域模型-資源庫 golang領域模型-依賴倒置 golang領域模型-聚合根 golang領域模型-CQRS golang領域模型-領域事件
項目代碼 https://github.com/8treenet/freedom/tree/master/example/fshop
推薦閱讀
站長 polarisxu
自己的原創(chuàng)文章
不限于 Go 技術
職場和創(chuàng)業(yè)經(jīng)驗
Go語言中文網(wǎng)
每天為你
分享 Go 知識
Go愛好者值得關注
