序言
筆者在《軟件設計的演變過程》一文中,將通信系統軟件的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再訪問方法。