Go 語言 DDD 實戰初級篇

導讀 

領域驅動設計 (DDD) 最簡潔的描述可能是:如何在明確的限界上下文中創建通用語言的模型。通過 DDD 思想設計開發的軟件,在領域專家、開發者和軟件本身之間不存在“翻譯”,三者通過在限界上下文下的通用語言直接表示。而這個系列則是我們團隊對 DDD 模式的探索和落地,旨在能幫助大家逐步揭開 DDD 的神祕面紗。

一、限界上下文

1.1 前言

DDD 分爲戰略設計和戰術設計,戰略設計就是劃分子域和限界上下文的過程。領域劃分爲子域的通用劃分形式是把領域劃分爲 核心子域、支撐子域、通用子域。我們在落地過程中常常會很容易劃分出核心子域,一般設計 mvp 的時候 mvp 就是核心子域。但是領域劃分出核心子域、支撐子域和通用子域之後就算劃分完成了嗎?

**1.2 **子域和限界上下文

實際上子域也是領域,一個公司不同部門關注的是一個大領域的不同子領域,在你關注的領域內也需要做這種子域的劃分。

比如百度這個大公司,有很多部門,這些部門都屬於互聯網領域,但是每個部門又有自己關注的領域,比如遊戲部門關注的是遊戲領域、搜索部門關注的是搜索領域。

不同部門的領域還可以再繼續劃分出自己關注的領域的核心域和支撐子域,所以整體上,領域的劃分就像一棵樹。我們回到自己關注的領域,基於這個領域做劃分。我們會把這個關注的領域劃分爲核心域、支撐域和通用域,一般每個域都由一個小團隊負責(康威定律)。

如果一個團隊的工作是支撐域,那麼這個支撐域就是他們的核心域,他們可以對此再做細緻的劃分,何時劃分到頭呢?用一個具體的限界上下文解決這個葉子領域的所有問題,並且領域通用語言在這個上下文中沒有二義性,那麼就算劃分到頭了。

劃分到樹葉的領域都是待解決的問題,也叫問題域,而限界上下文呢就是用來解決這個域內所有問題的模型。

針對限界上下文與領域的對應關係 Vernon 給出了建議,最好是 1:1 的關係,當然也有其他說法如 1:N,N:N,本人認同 Vernon 的說法,如果子域對應多個限界上下文,那麼只能說該子域還可以再劃分爲子子域,由子子域去對應每個限界上下文。劃分好子域和限界上下文後,限界上下文的主題就是解決這個子領域的問題,手段就是 DDD 戰術建模,工具就是領域通用語言,限制就是領域通用語言不能有二義性。

1.3 劃分領域(限界上下文)的依據

**1.4 **落地經驗

在落地過程中我們遇到了一個建模問題:

我們的服務有兩個角色使用:

在項目初期由於設計問題,最終放棄了拆分這兩個上下文,而是使用相同的上下文進行了建模。

這個問題的本質是我們沒有想好領域劃分,現在回頭想想,我們處理的是一個核心域,但是這個核心域又可以分爲兩個子域:一個是配置平臺子域,一個是用戶使用子域。

兩個子域的關注點是不同的,並且變化頻次也不同,後續用戶使用上下文會做橫向擴展,我們目前的單體架構雖然能做擴展,但是不符合單一職責原則,因爲用戶使用平臺集成了配置功能,而配置功能是不應該隨着用戶功能進行擴展的。在拆分過程中,會有很多代碼是重疊的,我們的服務中就有很多 Aggregate 聚合,在兩個上下文中有很多字段是一樣的,但重複並不一定是錯誤,因爲重複的代碼關注點和變化頻率是不一樣的。這裏我們介紹了利用角色進行關注點區分,進而劃分子域和限界上下文的方法,實際上也可以根據其他條件對領域進行劃分,劃分只要保證概念相對獨立,關注點相對獨立,劃分後沒有丟失問題就可以。

1.5 小結

  1. 領域就是有一個範圍,在這個範圍內有不同的角色,每個角色都有該角色應該具備的領域知識,各角色之間通過自己掌握的知識完成彼此協作,完成一些領域活動,產生一些領域事件,最終完成領域職責。

  2. 劃分領域的依據就是領域職責(目標)、領域關注點、完成職責需要的角色、角色需要的知識、角色需要執行的活動。

  3. 事件風暴的過程也是識別領域活動、領域職責、領域角色、領域事件、領域知識的過程。

二、實體

2.1 前言

實體是領域驅動設計中非常重要的一個部分,Len Silerston 說:“實體是一個重要的概念,企業希望建立和存儲的信息都是關於實體的信息”。在 DDD 中,實體的構建是重中之重。

2.2 什麼是實體

實體,是謂詞描述的主體。它包含了其他範疇,如引起屬性變化和狀態遷移的動作。一個典型的實體應該具有 3 個要素:

**2.3 **構建實體的依據

在 DDD 設計中,我們將開發者的視線從數據庫移到了實體上,以往我們在設計一個系統時,會關注要建立多少張表,而我們在 DDD 中,則需要關注如何建立實體,這兩者的異同點在於:

舉個例子

對於一個學生信息管理系統而言,我們設計了一個學生的實體。

type Student struct {
        ID     uint64
        Name   string
        Sex    string
        Class  string
        IsLate int
        Sign   *Sign
}
type Sign struct {
        SignTime time.Time
}
func (stu *Student) StudentSign() {
        isLate := TimeCheck()
        stu.IsLate = isLate
        // flush redis...
}

以上實體的結構可以簡單概括爲:

  1. 身份標識:ID

  2. 屬性:Name、Sex、Class、IsLate、值對象(Sign)

  3. 領域行爲:Sign()

在我們的數據庫設計中,Student 的基礎信息,可能只包括了 ID、Name、Sex、Class 這四個字段,那 IsLate 字段呢?我們將學生 IsLate 屬性寫進緩存裏,方便某些監察管理系統的高頻查詢,同時我們通過 Sign() 方法進行學生簽到狀態的變更,我們在 Sign 方法中進行校驗後,修改這個學生實體的 IsLate 屬性。

補充:

值對象也是實體對象的屬性之一,它沒有身份標識,也不可改變。比如上面的簽到,學生在今天簽到之後,創建的簽到記錄,就是學生的值對象,這條記錄創建了,就不可改變了(排除黑入教務系統篡改個人數據的情況)。值對象更多的信息,會在後面提到。

三、值對象

3.1 前言

值對象是實體的一個重要組成部分,如何正確使用值對象,也是 DDD 領域驅動設計的一個難題。本文將介紹值對象的概念與使用方法。

3.2 概念

值對象是實體對象的屬性,通常代表分量、性質、關係、場所、時間或位置 / 姿態。當實體屬性需要表現出其屬性的意義,併爲這個意義提供相關功能,可以設置爲值對象。比如一家公司所在的省 / 市 / 區 / 街道可以合成值對象表示這家公司的地址屬性。

3.3 特點

3.4 領域行爲

那什麼是值對象的領域行爲呢?

// NewCoordinateVo 初始化座標值對象
func NewCoordinateVo(LongitudeStr string, LatitudeStr string) (*VoCoordinate, error) {
  // 自我驗證
  Longitude, err := strconv.ParseFloat(LongitudeStr, 64)
  if err != nil {
    return nil, fmt.Errorf("Longitude_input_err")
  }
  Latitude, err := strconv.ParseFloat(LatitudeStr, 64)
  if err != nil {
    return nil, fmt.Errorf("Latitude_input_err")
  }
  return &VoCoordinate{
    Longitude: Longitude,
    Latitude:  Latitude,
  }, nil
}

3.5 F&Q

1、相比於普通屬性,值對象有哪些優勢呢?

可以展現領域概念;學生實體的年齡,string 與 Name、int 與 Age 相比,顯然後者更加直觀得體現了業務含義。可以封裝顯而易見的領域概念;比如對於一個經銷商 4s 店店位置經度和緯度都是這個 4s 店實體實體店屬性,但是合成一個座標值對象更能展示實體店領域概念。更好的封裝利於自我領域行爲的驗證能力。保證每次生成得值對象都是正確的。

2. 那麼一個領域的概念我們用實體還是值對象呢?可以依據幾點來判斷?

業務對它相等的判斷是根據值還是身份標識。前者是值對象,後者是實體。當我們從圖書館判斷一本書是否相同,即使名字相同也並非同一本書,在系統中,只有 id 相同纔是同一本書;但我們判斷一個位置,當經緯度相同的時候就是同一個位置。這個時候圖書就是定義爲實體,座標定義爲值對象。

確定對象的屬性值是否會發生變化,如果變化了,究竟是產生一個完全不同的對象,還是維持相同的身份標識。在員工的出勤記錄業務場景中,依據相等性進行判斷時,可以任務出勤記錄值相等的就是同一條記錄,但如果員工提出補卡,對記錄狀態修改對時候,其同一性就只能通過唯一的身份標識進行判斷,這意味這應該被定義爲實體。

生命週期是手動的。值對象沒有身份標識,意味着無需管理其生命週期。但是實體無需關注。

多個判斷條件是層層遞進的,要確定一個領域概念究竟是實體還是值對象,需要謹慎判斷,綜合考量。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/FEd9Z48R8FKuRrorLZzvJA