序言
笔者在《软件设计的演变过程》一文中,将通信系统软件的DDD分层模型最终演进为五层模型,即调度层(Schedule)、事务层(Transaction DSL)、环境层(Context)、领域层(Domain)和基础设施层(Infrastructure),我们简单回顾一下:
- 调度层:维护UE的状态模型,只包括业务的本质状态,将接收到的消息派发给事务层。
- 事务层:对应一个业务流程,比如UE Attach,将各个同步消息或异步消息的处理组合成一个事务,当事务失败时,进行回滚。当事务层收到调度层的消息后,委托环境层的Action进行处理。
- 环境层:以Action为单位,处理一条同步消息或异步消息,将Domain层的领域对象cast成合适的role,让role交互起来完成业务逻辑。
- 领域层:不仅包括领域对象及其之间关系的建模,还包括对象的角色role的显式建模。
- 基础实施层:为其他层提供通用的技术能力,比如消息通信机制、对象持久化机制和通用的算法等。
对于业务来说,事务层和领域层都非常重要。笔者在《Golang事务模型》一文中重点讨论了事务层,本文主要阐述领域层的实现技术,将通过一个案例逐步展开。
本文使用的案例源自MagicBowen的一篇热文《DCI in C++》,并做了一些修改,目的是将Golang版领域对象的主要实现技术尽可能流畅的呈现给读者。
领域对象的实现
假设有这样一种场景:模拟人和机器人制造产品。人制造产品会消耗吃饭得到的能量,缺乏能量后需要再吃饭补充;而机器人制造产品会消耗电能,缺乏能量后需要再充电。这里人和机器人在工作时都是一名工人,工作的流程是一样的,但是区别在于依赖的能量消耗和获取方式不同。
领域模型
通过对场景进行分析,我们根据组合式设计的基本思想得到一个领域模型:
物理设计
从领域模型中可以看出,角色Worker既可以组合在领域对象Human中,又可以组合在领域对象Robot中,可见领域对象和角色是两个不同的变化方向,于是domain的子目录结构为:
role的实现
Energy
Energy是一个抽象role,在Golang中是一个interface。它包含两个方法:一个是消耗能量Consume,另一个是能量是否耗尽IsExhausted。
Energy的代码比较简单,如下所示:
package role
type Energy interface {
Consume()
IsExhausted() bool
}
HumanEnergy
HumanEnergy是一个具体role,在Golang中是一个struct。它既有获取能量的吃饭方法Eat,又实现了接口Energy的所有方法。对于HumanEnergy来说,Eat一次获取的所有能量在Consume 10次后就完全耗尽。
HumanEnergy的代码如下所示:
package role
type HumanEnergy struct {
isHungry bool
consumeTimes int
}
const MAX_CONSUME_TIMES = 10
func (h *HumanEnergy) Eat() {
h.consumeTimes = 0
h.isHungry = false
}
func (h *HumanEnergy) Consume() {
h.consumeTimes++
if h.consumeTimes >= MAX_CONSUME_TIMES {
h.isHungry = true
}
}
func (h *HumanEnergy) IsExhausted() bool {
return h.isHungry
}
RobotEnergy
RobotEnergy是一个具体role,在Golang中是一个struct。它既有获取能量的充电方法Charge,又实现了接口Energy的所有方法。对于RobotEnergy来说,Charge一次获取的所有能量在Consume 100次后就完全耗尽。
RobotEnergy的代码如下所示:
package role
type RobotEnergy struct {
percent int
}
const (
FULL_PERCENT = 100
CONSUME_PERCENT = 1
)
func (r *RobotEnergy) Charge() {
r.percent = FULL_PERCENT
}
func (r *RobotEnergy) Consume() {
if r.percent > 0 {
r.percent -= CONSUME_PERCENT
}
}
func (r *RobotEnergy) IsExhausted() bool {
return r.percent == 0
}
Worker
Worker是一名工人,人和机器人在工作时都是一名Worker,工作的流程是一样的,但是区别在于依赖的能量消耗和获取方式不同。对于代码实现来说Worker仅依赖于另一个角色Energy,只有在Worker的实例化阶段才需要考虑注入Energy的依赖。
Worker是一个具体role,在Golang中是一个struct。它既有生产产品的方法Produce,又有获取已生产的产品数的方法GetProduceNum。
Worker的代码如下所示:
package role
type Worker struct {
produceNum int
Energy Energy
}
func (w *Worker) Produce() {
if w.Energy.IsExhausted() {
return
}
w.produceNum++
w.Energy.Consume()
}
func (w *Worker) GetProduceNum() int {
return w.produceNum
}
领域对象的实现
该案例中有两个领域对象,一个是Human,另一个是Robot。我们知道,在C++中通过多重继承来完成领域对象和其支持的role之间的关系绑定,同时在多重继承树内通过关系交织来完成role之间的依赖关系描述。这种方式在C++中比采用传统的依赖注入的方式更加简单高效,所以在Golang中我们尽量通过模拟C++中的多重继承来实现领域对象,而不是仅仅靠简陋的委托。
在Golang中可以通过匿名组合来模拟C++中的多重继承,role之间的依赖注入不再是注入具体role,而是将领域对象直接注入,可以避免产生很多小对象。
在我们的案例中,角色Worker依赖于抽象角色Energy,所以在实例化Worker时,要么注入HumanEnergy,要么注入RobotEnergy,这就需要产生具体角色的对象(小对象)。领域对象Human在工作时是一名Worker,消耗的是通过吃饭获取的能量,所以Human通过HumanEnergy和Worker匿名组合而成。Golang通过了匿名组合实现了继承,那么就相当于Human多重继承了HumanEnergy和Worker,即Human也实现了Energy接口,那么给Energy注入Human就等同于注入了HumanEnergy,同时避免了小对象HumanEnergy的创建。同理,Robot通过RobotEnergy和Worker匿名组合而成,Worker中的Energy注入的是Robot。
Human的实现
Human对象中有一个方法inject用于role的依赖注入,Human对象的创建通过工厂函数CreateHuman实现。
Human的代码如下所示:
package object
import(
"domain/role"
)
type Human struct {
role.HumanEnergy
role.Worker
}
func (h *Human) inject() {
h.Energy = h
}
func CreateHuman() *Human {
h := &Human{}
h.inject()
return h
}
Robot的实现
同理,Robot对象中有一个方法inject用于role的依赖注入,Robot对象的创建通过工厂函数CreateRobot实现。
Robot的代码如下所示:
package object
import(
"domain/role"
)
type Robot struct {
role.RobotEnergy
role.Worker
}
func (r *Robot) inject() {
r.Energy = r
}
func CreateRobot() *Robot {
r := &Robot{}
r.inject()
return r
}
领域对象的使用
在Context层中,对于任一个Action,都有明确的场景使得领域对象cast成该场景的role,并通过role的交互完成Action的行为。在Golang中对于匿名组合的struct,默认的变量名就是该struct的名字。当我们访问该struct的方法时,既可以直接访问(略去默认的变量名),又可以通过默认的变量名访问。我们推荐通过默认的变量名访问,从而将role显式化表达出来。由此可见,在Golang中领域对象cast成role的方法非常简单,我们仅仅借助这个默认变量的特性就可直接访问role。
HumanProduceInOneCycleAction
对于Human来说,一个生产周期就是HumanEnergy角色Eat一次获取的能量被角色Worker生产产品消耗的过程。HumanProduceInOneCycleAction是针对这个过程的一个Action,代码实现简单模拟如下:
package context
import (
"fmt"
"domain/object"
)
func HumanProduceInOneCycleAction() {
human := object.CreateHuman()
human.HumanEnergy.Eat()
for {
human.Worker.Produce()
if human.HumanEnergy.IsExhausted() {
break
}
}
fmt.Printf("human produce %v products in one cycle\n", human.Worker.GetProduceNum())
}
打印如下:
human produce 10 products in one cycle
符合预期!
RobotProduceInOneCycleAction
对于Robot来说,一个生产周期就是RobotEnergy角色Charge一次获取的能量被角色Worker生产产品消耗的过程。RobotProduceInOneCycleAction是针对这个过程的一个Action,代码实现简单模拟如下:
package context
import (
"fmt"
"domain/object"
)
func RobotProduceInOneCycleAction() {
robot := object.CreateRobot()
robot.RobotEnergy.Charge()
for {
robot.Worker.Produce()
if robot.RobotEnergy.IsExhausted() {
break
}
}
fmt.Printf("robot produce %v products in one cycle\n", robot.Worker.GetProduceNum())
}
打印如下:
robot produce 100 products in one cycle
符合预期!
小结
本文通过一个案例阐述了Golang中领域对象的实现要点,我们归纳如下:
- 类是一种模块化的手段,遵循高内聚低耦合,让软件易于应对变化,对应role;对象作为一种领域对象的直接映射,解决了过多的类带来的可理解性问题,领域对象由role组合而成。
- 领域对象和角色是两个不同的变化方向,我们在做物理设计时应该是两个并列的目录。
- 通过匿名组合实现多重继承。
- role的依赖注入单位是领域对象,而不是具体role。
- 使用领域对象时,不要直接访问role的方法,而是先cast成role再访问方法。