序言
筆者在《軟件設(shè)計(jì)的演變過程》一文中渠鸽,將通信系統(tǒng)軟件的DDD分層模型最終演進(jìn)為五層模型下硕,即調(diào)度層(Schedule)弄息、事務(wù)層(Transaction DSL)尔崔、環(huán)境層(Context)、領(lǐng)域?qū)?Domain)和基礎(chǔ)設(shè)施層(Infrastructure)呛伴,我們簡單回顧一下:
- 調(diào)度層:維護(hù)UE的狀態(tài)模型勃痴,只包括業(yè)務(wù)的本質(zhì)狀態(tài),將接收到的消息派發(fā)給事務(wù)層热康。
- 事務(wù)層:對(duì)應(yīng)一個(gè)業(yè)務(wù)流程沛申,比如UE Attach,將各個(gè)同步消息或異步消息的處理組合成一個(gè)事務(wù)姐军,當(dāng)事務(wù)失敗時(shí)铁材,進(jìn)行回滾尖淘。當(dāng)事務(wù)層收到調(diào)度層的消息后,委托環(huán)境層的Action進(jìn)行處理著觉。
- 環(huán)境層:以Action為單位村生,處理一條同步消息或異步消息,將Domain層的領(lǐng)域?qū)ο骳ast成合適的role饼丘,讓role交互起來完成業(yè)務(wù)邏輯趁桃。
- 領(lǐng)域?qū)樱翰粌H包括領(lǐng)域?qū)ο蠹捌渲g關(guān)系的建模,還包括對(duì)象的角色role的顯式建模肄鸽。
- 基礎(chǔ)實(shí)施層:為其他層提供通用的技術(shù)能力卫病,比如消息通信機(jī)制、對(duì)象持久化機(jī)制和通用的算法等典徘。
對(duì)于業(yè)務(wù)來說蟀苛,事務(wù)層和領(lǐng)域?qū)佣挤浅V匾9P者在《Golang事務(wù)模型》一文中重點(diǎn)討論了事務(wù)層烂斋,本文主要闡述領(lǐng)域?qū)拥膶?shí)現(xiàn)技術(shù)屹逛,將通過一個(gè)案例逐步展開。
本文使用的案例源自MagicBowen的一篇熱文《DCI in C++》汛骂,并做了一些修改罕模,目的是將Golang版領(lǐng)域?qū)ο蟮闹饕獙?shí)現(xiàn)技術(shù)盡可能流暢的呈現(xiàn)給讀者。
領(lǐng)域?qū)ο蟮膶?shí)現(xiàn)
假設(shè)有這樣一種場(chǎng)景:模擬人和機(jī)器人制造產(chǎn)品帘瞭。人制造產(chǎn)品會(huì)消耗吃飯得到的能量淑掌,缺乏能量后需要再吃飯補(bǔ)充;而機(jī)器人制造產(chǎn)品會(huì)消耗電能蝶念,缺乏能量后需要再充電抛腕。這里人和機(jī)器人在工作時(shí)都是一名工人,工作的流程是一樣的媒殉,但是區(qū)別在于依賴的能量消耗和獲取方式不同担敌。
領(lǐng)域模型
通過對(duì)場(chǎng)景進(jìn)行分析,我們根據(jù)組合式設(shè)計(jì)的基本思想得到一個(gè)領(lǐng)域模型:
物理設(shè)計(jì)
從領(lǐng)域模型中可以看出廷蓉,角色Worker既可以組合在領(lǐng)域?qū)ο驢uman中全封,又可以組合在領(lǐng)域?qū)ο驲obot中,可見領(lǐng)域?qū)ο蠛徒巧莾蓚€(gè)不同的變化方向桃犬,于是domain的子目錄結(jié)構(gòu)為:
role的實(shí)現(xiàn)
Energy
Energy是一個(gè)抽象role刹悴,在Golang中是一個(gè)interface。它包含兩個(gè)方法:一個(gè)是消耗能量Consume攒暇,另一個(gè)是能量是否耗盡IsExhausted土匀。
Energy的代碼比較簡單,如下所示:
package role
type Energy interface {
Consume()
IsExhausted() bool
}
HumanEnergy
HumanEnergy是一個(gè)具體role形用,在Golang中是一個(gè)struct就轧。它既有獲取能量的吃飯方法Eat证杭,又實(shí)現(xiàn)了接口Energy的所有方法。對(duì)于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是一個(gè)具體role,在Golang中是一個(gè)struct携丁。它既有獲取能量的充電方法Charge,又實(shí)現(xiàn)了接口Energy的所有方法兰怠。對(duì)于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是一名工人揭保,人和機(jī)器人在工作時(shí)都是一名Worker肥橙,工作的流程是一樣的,但是區(qū)別在于依賴的能量消耗和獲取方式不同秸侣。對(duì)于代碼實(shí)現(xiàn)來說Worker僅依賴于另一個(gè)角色Energy存筏,只有在Worker的實(shí)例化階段才需要考慮注入Energy的依賴。
Worker是一個(gè)具體role味榛,在Golang中是一個(gè)struct椭坚。它既有生產(chǎn)產(chǎn)品的方法Produce,又有獲取已生產(chǎn)的產(chǎn)品數(shù)的方法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
}
領(lǐng)域?qū)ο蟮膶?shí)現(xiàn)
該案例中有兩個(gè)領(lǐng)域?qū)ο笊凭ィ粋€(gè)是Human,另一個(gè)是Robot频轿。我們知道垂涯,在C++中通過多重繼承來完成領(lǐng)域?qū)ο蠛推渲С值膔ole之間的關(guān)系綁定,同時(shí)在多重繼承樹內(nèi)通過關(guān)系交織來完成role之間的依賴關(guān)系描述航邢。這種方式在C++中比采用傳統(tǒng)的依賴注入的方式更加簡單高效耕赘,所以在Golang中我們盡量通過模擬C++中的多重繼承來實(shí)現(xiàn)領(lǐng)域?qū)ο螅皇莾H僅靠簡陋的委托膳殷。
在Golang中可以通過匿名組合來模擬C++中的多重繼承操骡,role之間的依賴注入不再是注入具體role,而是將領(lǐng)域?qū)ο笾苯幼⑷牖嘀梢员苊猱a(chǎn)生很多小對(duì)象当娱。
在我們的案例中,角色Worker依賴于抽象角色Energy考榨,所以在實(shí)例化Worker時(shí)跨细,要么注入HumanEnergy,要么注入RobotEnergy河质,這就需要產(chǎn)生具體角色的對(duì)象(小對(duì)象)冀惭。領(lǐng)域?qū)ο驢uman在工作時(shí)是一名Worker震叙,消耗的是通過吃飯獲取的能量,所以Human通過HumanEnergy和Worker匿名組合而成散休。Golang通過了匿名組合實(shí)現(xiàn)了繼承媒楼,那么就相當(dāng)于Human多重繼承了HumanEnergy和Worker,即Human也實(shí)現(xiàn)了Energy接口戚丸,那么給Energy注入Human就等同于注入了HumanEnergy划址,同時(shí)避免了小對(duì)象HumanEnergy的創(chuàng)建。同理限府,Robot通過RobotEnergy和Worker匿名組合而成夺颤,Worker中的Energy注入的是Robot。
Human的實(shí)現(xiàn)
Human對(duì)象中有一個(gè)方法inject用于role的依賴注入胁勺,Human對(duì)象的創(chuàng)建通過工廠函數(shù)CreateHuman實(shí)現(xiàn)世澜。
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的實(shí)現(xiàn)
同理,Robot對(duì)象中有一個(gè)方法inject用于role的依賴注入署穗,Robot對(duì)象的創(chuàng)建通過工廠函數(shù)CreateRobot實(shí)現(xiàn)寥裂。
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
}
領(lǐng)域?qū)ο蟮氖褂?/h2>
在Context層中,對(duì)于任一個(gè)Action案疲,都有明確的場(chǎng)景使得領(lǐng)域?qū)ο骳ast成該場(chǎng)景的role封恰,并通過role的交互完成Action的行為。在Golang中對(duì)于匿名組合的struct络拌,默認(rèn)的變量名就是該struct的名字俭驮。當(dāng)我們?cè)L問該struct的方法時(shí),既可以直接訪問(略去默認(rèn)的變量名)春贸,又可以通過默認(rèn)的變量名訪問混萝。我們推薦通過默認(rèn)的變量名訪問,從而將role顯式化表達(dá)出來萍恕。由此可見逸嘀,在Golang中領(lǐng)域?qū)ο骳ast成role的方法非常簡單,我們僅僅借助這個(gè)默認(rèn)變量的特性就可直接訪問role允粤。
HumanProduceInOneCycleAction
對(duì)于Human來說崭倘,一個(gè)生產(chǎn)周期就是HumanEnergy角色Eat一次獲取的能量被角色Worker生產(chǎn)產(chǎn)品消耗的過程。HumanProduceInOneCycleAction是針對(duì)這個(gè)過程的一個(gè)Action类垫,代碼實(shí)現(xiàn)簡單模擬如下:
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
符合預(yù)期司光!
RobotProduceInOneCycleAction
對(duì)于Robot來說,一個(gè)生產(chǎn)周期就是RobotEnergy角色Charge一次獲取的能量被角色Worker生產(chǎn)產(chǎn)品消耗的過程悉患。RobotProduceInOneCycleAction是針對(duì)這個(gè)過程的一個(gè)Action残家,代碼實(shí)現(xiàn)簡單模擬如下:
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
符合預(yù)期!
小結(jié)
本文通過一個(gè)案例闡述了Golang中領(lǐng)域?qū)ο蟮膶?shí)現(xiàn)要點(diǎn)售躁,我們歸納如下:
- 類是一種模塊化的手段坞淮,遵循高內(nèi)聚低耦合茴晋,讓軟件易于應(yīng)對(duì)變化,對(duì)應(yīng)role回窘;對(duì)象作為一種領(lǐng)域?qū)ο蟮闹苯佑成渑瞪茫鉀Q了過多的類帶來的可理解性問題,領(lǐng)域?qū)ο笥蓃ole組合而成啡直。
- 領(lǐng)域?qū)ο蠛徒巧莾蓚€(gè)不同的變化方向烁涌,我們?cè)谧鑫锢碓O(shè)計(jì)時(shí)應(yīng)該是兩個(gè)并列的目錄。
- 通過匿名組合實(shí)現(xiàn)多重繼承酒觅。
- role的依賴注入單位是領(lǐng)域?qū)ο笈胗瘢皇蔷唧wrole。
- 使用領(lǐng)域?qū)ο髸r(shí)阐滩,不要直接訪問role的方法,而是先cast成role再訪問方法县忌。