1.理解实体
实体具有唯一的身份,比如在订单被接收了
,订单被配送了
,在订单业务这样的生命周期中,就是需要订单这个实体来承载相应的状态和事件。实体的识别和领域有关系,比如对于金额
大多数情况都是值对象,比如存入了100美元和去除100美元都是等效的,不一定非要取出存入的钱,但是对钞票印刷或者钞票可追溯的行业就需要定义为实体。
2.实现实体
1.唯一标识
对应问题域中已经有的唯一标识符号可以用自然键,比如图书的ISBN,人的身份证号,没有的可以采用ID生成器去生成。
很多业务根本拿不到自然键或者自然键是可选项,都是自生成ID。
2.将行为推入值对象和领域服务中
- 一开始表现为归属实体的无状态领域操作可以放在领域服务中
- 能交给值对象处理的逻辑,可以放在值对象里面。
一个实体HolidayBook
,表示一个人的假期的预定计划,原本包含user_id、start_time、end_time、book_time(表示约定被确认的时间),但是开始时间和结束时间有些业务逻辑,比如start小于end,并且end-start要大于3天,定义了一个stay值对象来包含start、end,用值对象实现了这些逻辑。避免了实体相关的代码中增加了stay这样的代码降低身份的表述性。
3.验证并强制不变性
和值对象类似,在设置属性时要根据值业务逻辑做验证是否可以设置目标值,比如订单状态已取消的不能被支付。
4.专注行为,而非数据。
采用告知而非询问原则:要直接告诉被调用方结果,而不是让调用方拿到基本数据自己在计算。案例举了一个足球比赛结果的实体,第一版提供了tem1_score和team2_score两个公开属性,让调用方判断,修改后提供了一个获取获胜方分数的方法,方法内部实现了多种不同的规则判断比如平局的情况下。
5.避免对完全对现实世界建模
对自己的领域创建实体,不必完全符合现实。有的时候实体是基于UI需求定义的,任何将实体转换成UI的地方中间用DTO对象来转换,避免损坏实体的接口和职责
。
5.分布式设计
比如一个消费者用户实体,他有订单列表、地址信息列表、忠诚度属性,但是分布式系统中这些分布在不同的上下文中,这个时候就需要调用不同的服务分别对属性赋值,每个服务提供根据用户id查询相关信息的接口。
3.常见实体建模的原则和模式
3.1 使用规范实现验证和不变条件
将验证规则放在单独的类实现,不同的业务场景用不同的验证规则。
3.2. 避免状态模式,使用显示建模
来一个外卖订单状态用状态模式实现的方式
// State表示订单的状态,里面定义的方法表示订单状态变化过程中的可能发生的动作
type State interface {
Cook() error // 烹饪
TakeOutOfOven() error //从烤箱取出
Package() error //打包
Deliver() error // 配送
}
type Order struct {
Id uint64
address string
state State
}
func (order *Order) setState(state State) {
order.state = state
}
func NewTakeawayOrder(id uint64, address string) *Order {
order := &Order{
Id: id,
address: address,
}
order.state = NewInKitchenQueue(order)
return order
}
// 从厨房中排队
type InKitchenQueue struct {
State
order *Order
}
func NewInKitchenQueue(order *Order) State {
return &InKitchenQueue{
order: order,
}
}
// 放在烤箱中
type InOven struct {
State
order *Order
}
func NewInOven(order *Order) State {
return &InOven{
order: order,
}
}
func (state *InKitchenQueue) Cook() error {
// 因为cook这个动作,将状态转换成为在烤箱中
// 这里可以再Order对每个状态增加属性并提前赋值,然后增加一个currentState属性,每次setState用这些属性值
state.order.setState(NewInOven(state.order))
return nil
}
func (state *InKitchenQueue) TakeOutOfOven() error {
return errors.New("now allow")
}
func (state *InKitchenQueue) Package() error {
return errors.New("now allow")
}
func (state *InKitchenQueue) Deliver() error {
return errors.New("now allow")
}
// 烹饪中
func (order *Order) Cook() error {
return order.state.Cook()
}
// 从烤箱取出
func (order *Order) TakeOutOfOven() error {
return order.state.TakeOutOfOven()
}
// 包装好
func (order *Order) Package() error {
return order.state.Package()
}
// 配送
func (order *Order) Deliver() error {
return order.state.Deliver()
}
缺点
- 包含大量模板和未实现的方法
- 状态缺乏明确性,允许使用未被不应该被调用的方法,比如上面调用后根据err返回判断是否成功,好的实现是在写代码的时候让不该存在的调用根本无法被开发者调用到。
下面是DDD提到的显示状态建模的方式,不同状态的订单继承了订单,然后每个子类内部自己实现可以调用的动作,相当于以前的State接口方法被拆分了。
// 这些接口可有可无,如果两个状态确实可以有相同的动作,对代码有约束作用。
type StateCook interface {
Cook() error // 烹饪
}
type StateTakeOutOfOven interface {
TakeOutOfOven() error // 烹饪
}
type Package interface {
TakeOutOfOven() error // 烹饪
}
type Deliver interface {
TakeOutOfOven() error // 烹饪
}
type Order struct {
Id uint64
address string
}
type InKitchenOrder struct {
Order
}
type InOvenOrder struct {
Order
}
// Cook只绑定在Inkitchen状态的订单上,同理其他状态依次类推。
func (order*InKitchenOrder) Cook() *InOvenOrder {
return &InOvenOrder{
Order{
Id: order.Id,
address: order.address,
},
}
}
描述状态是否可以从一个状态转向另一个状态不难实现,用于一个二维的map就可以,但是要包含动作信息,现实的表达出因为某个动作触发了状态的流转是必要的,这个动作表现就是类的一个方法,方法内部也包含了其他的业务逻辑。
3.3 避免备忘录模式和set、get方法一起用
尽量避免书写set、get方法公开属性,有利于创建基于行为的领域模型,但是有的时候就是需要获取属性就不得不写Get方法,但是必要的时候可以选择备忘录模式。
type ShopCart struct {
items []string
userId int64
shopId int64
}
func (c * ShopCart) GetUserId() int64 {
return c.userId
}
func (c * ShopCart) GetSnapshot() *CartSnapshot {
return &CartSnapshot{
Items: c.items,
ShopId: c.shopId,
}
}
type CartSnapshot struct {
Items []string
ShopId int64
}
通过快照,cart类依然维持了与其他应用程序的隔离封装。
3.4 定义无隐藏意外影响的函数
比如不能定义一个函数名字为Query
,内部却进行了更新操作,就是函数命名要和实际实现功能匹配。
网友评论