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 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中领域对象的实现要点,我们归纳如下:

  1. 类是一种模块化的手段,遵循高内聚低耦合,让软件易于应对变化,对应role;对象作为一种领域对象的直接映射,解决了过多的类带来的可理解性问题,领域对象由role组合而成。
  2. 领域对象和角色是两个不同的变化方向,我们在做物理设计时应该是两个并列的目录。
  3. 通过匿名组合实现多重继承。
  4. role的依赖注入单位是领域对象,而不是具体role。
  5. 使用领域对象时,不要直接访问role的方法,而是先cast成role再访问方法。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,980评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,178评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,868评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,498评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,492评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,521评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,910评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,569评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,793评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,559评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,639评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,342评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,931评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,904评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,144评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,833评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,350评论 2 342

推荐阅读更多精彩内容