为 Gopher 打造 DDD 系列:领域模型-实体
前言: 实体具有业务属性、业务逻辑和业务行为,是是实实在在的业务对象。在事件风暴中,我们可以根据命令、操作与事件将业务上紧密结合在一起的多个实体与值对象进行聚合形成聚合根。
实体是什么
虽然数据库的设计占据了主导地位(这个是没错的),但开发者也不应该只关注数据,而且要关注模型。
数据+行为= 模型,实体就是含有领域概念的模型。它是一个唯一的东西,在相当长的时间里数据状态在持续地变化,并且一定有唯一键,这区别于值对象。
注意的是如果非要用表结构里的一条含有主键的数据去理解实体也是可以的,但不少情况下可能是有多个表或者k/v数据来组成的一个实体。
实体、值对象与数据模型示例

实体是可变的,是变性,每个用户实体都有自己的唯一性,我们用id来进行区分。值对象是不变的,是共性,实体都有相同的值对象,例如国家等信息。我们以此区分好实体与值对象。
为什么使用实体
如果开发者设计系统时,并没有建模而是直接开始设计表结构和它的CRUD,其实这也算是建模的一种,但这种方式仅仅能应对简单的模型。这样的操作当更复杂的业务和更复杂的模型出现后是驾驭不了的。假设我们做10平米的卧室设计,那么简单的量一下桌椅床就ok。但如果我们设计一个200平米的化学实验室的时候,这个简单的摆放可能搞不好会导致爆炸吧~
实体的数据与行为
唯一标识
在实体的设计早期,我们将关注点都放在了实体的身份唯一性、属性、行为上。同时还应该关注对实体的查询和创建,我们首先要考虑实体的本质特征,特别是实体的唯一标识符,它是一个关系节点。比如用户这个实体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方法。
发布领域事件
type Entity interface {//发布领域事件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" //发货)// 订单实体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对象的方法,设置状态}// Shipment 发货func (o *Order) Shipment() {o.SetStatus(OrderStatusShipment) //po对象的方法,设置状态}// 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
自己的原创文章
不限于 Go 技术
职场和创业经验
Go语言中文网
每天为你
分享 Go 知识
Go爱好者值得关注
