這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
序言
筆者在《軟體設計的演變過程》一文中,將通訊系統軟體的DDD分層模型最終演化為五層模型,即調度層(Schedule)、事務層(Transaction DSL)、環境層(Context)、領域層(Domain)和基礎設施層(Infrastructure),我們簡單回顧一下:
ddd-layer-with-dci-dsl.png
- 調度層:維護UE的狀態模型,只包括業務的本質狀態,將接收到的訊息派發給事務層。
- 事務層:對應一個商務程序,比如UE Attach,將各個同步訊息或非同步訊息的處理組合成一個事務,當事務失敗時,進行復原。當事務層收到調度層的訊息後,委託環境層的Action進行處理。
- 環境層:以Action為單位,處理一條同步訊息或非同步訊息,將Domain層的領域對象cast成合適的role,讓role互動起來完成商務邏輯。
- 領域層:不僅包括領域對象及其之間關係的建模,還包括對象的角色role的顯式建模。
- 基礎實施層:為其他層提供通用的技術能力,比如訊息通訊機制、對象持久化機制和通用的演算法等。
對於業務來說,事務層和領域層都非常重要。筆者在《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 roletype Energy interface { Consume() IsExhausted() bool}
HumanEnergy
HumanEnergy是一個具體role,在Golang中是一個struct。它既有擷取能量的吃飯方法Eat,又實現了介面Energy的所有方法。對於HumanEnergy來說,Eat一次擷取的所有能量在Consume 10次後就完全耗盡。
HumanEnergy的代碼如下所示:
package roletype HumanEnergy struct { isHungry bool consumeTimes int}const MAX_CONSUME_TIMES = 10func (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 roletype 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 roletype 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 objectimport( "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 objectimport( "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 contextimport ( "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 contextimport ( "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再存取方法。