Implement Domain Object in Golang

时间:2022-05-06
本文章向大家介绍Implement Domain Object in Golang,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

序言

笔者在《软件设计的演变过程》一文中,将通信系统软件的DDD分层模型最终演进为五层模型,即调度层(Schedule)、事务层(Transaction DSL)、环境层(Context)、领域层(Domain)和基础设施层(Infrastructure),我们简单回顾一下:

ddd-layer-with-dci-dsl.png

  1. 调度层:维护UE的状态模型,只包括业务的本质状态,将接收到的消息派发给事务层。
  2. 事务层:对应一个业务流程,比如UE Attach,将各个同步消息或异步消息的处理组合成一个事务,当事务失败时,进行回滚。当事务层收到调度层的消息后,委托环境层的Action进行处理。
  3. 环境层:以Action为单位,处理一条同步消息或异步消息,将Domain层的领域对象cast成合适的role,让role交互起来完成业务逻辑。
  4. 领域层:不仅包括领域对象及其之间关系的建模,还包括对象的角色role的显式建模。
  5. 基础实施层:为其他层提供通用的技术能力,比如消息通信机制、对象持久化机制和通用的算法等。

对于业务来说,事务层和领域层都非常重要。笔者在《Golang事务模型》一文中重点讨论了事务层,本文主要阐述领域层的实现技术,将通过一个案例逐步展开。

本文使用的案例源自MagicBowen的一篇热文《DCI in C++》,并做了一些修改,目的是将Golang版领域对象的主要实现技术尽可能流畅的呈现给读者。

领域对象的实现

假设有这样一种场景:模拟人和机器人制造产品。人制造产品会消耗吃饭得到的能量,缺乏能量后需要再吃饭补充;而机器人制造产品会消耗电能,缺乏能量后需要再充电。这里人和机器人在工作时都是一名工人,工作的流程是一样的,但是区别在于依赖的能量消耗和获取方式不同。

领域模型

通过对场景进行分析,我们根据组合式设计的基本思想得到一个领域模型:

human-robot.png

物理设计

从领域模型中可以看出,角色Worker既可以组合在领域对象Human中,又可以组合在领域对象Robot中,可见领域对象和角色是两个不同的变化方向,于是domain的子目录结构为:

object-role-dir.png

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 cyclen", 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 cyclen", robot.Worker.GetProduceNum())

}

打印如下:

robot produce 100 products in one cycle

符合预期!

小结

本文通过一个案例阐述了Golang中领域对象的实现要点,我们归纳如下:

  1. 类是一种模块化的手段,遵循高内聚低耦合,让软件易于应对变化,对应role;对象作为一种领域对象的直接映射,解决了过多的类带来的可理解性问题,领域对象由role组合而成。
  2. 领域对象和角色是两个不同的变化方向,我们在做物理设计时应该是两个并列的目录。
  3. 通过匿名组合实现多重继承。
  4. role的依赖注入单位是领域对象,而不是具体role。
  5. 使用领域对象时,不要直接访问role的方法,而是先cast成role再访问方法。