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再訪問方法。
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容