Go語言DDD實戰初級篇

語言: CN / TW / HK

導讀

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

全文5259字,預計閲讀時間14分鐘。

一、限界上下文

1.1 前言

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

1.2 子域和限界上下文

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

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

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

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

圖片

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

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

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

  • 通用語言:在做領域劃分之前一定要統一領域通用語言,如果一個名詞在用語言描述的時候在不同語境有不同含義,那麼就應該在不同語境中創建不同上下文。比如book,在寫作階段就是草稿,在出版階段就是一個出版物,在購買階段是一個書籍類商品,在發貨階段是一個物流訂單。那麼就應該按照書的角色進行歸類,區分出上下文。正所謂,在商言商,在領域就應該説領域的通用語言。

  • 領域職責:不同領域想要達成的目標是不一樣的,每個領域都有自己最終要完成的事情,即通過領域知識,完成領域活動,最後完成領域職責。

  • 領域角色:不同領域的角色也不盡相同,前端領域裏可能需要ue角色、fe角色。後端領域裏可能需要java研發、dba等角色。同時上邊舉的book的例子,book在不同領域的角色也是不一樣的。再通俗一點,你在學校是學生角色,上班是員工角色。

  • 領域知識:不同領域包括的知識是不一樣的,比如後端和前端,後端可能需要了解服務器相關的知識、前端需要了解的是界面相關的知識。

  • 領域活動:不同領域的職責也是不同的,在領域內進行的活動也不一樣,比如前端需要構建前端頁面活動。後端處理數據庫交互活動。領域活動會利用到領域知識,如果進行領域活動的時候卻不具備這個領域的知識,那麼説明領域劃分是不合理的。

  • 領域關注點:不同領域關注點不同,拿person舉例,person有身份證信息、年齡、身高、體重、工作、專業等信息,但是在不同領域對person的關注點是不一樣的,銀行辦信用卡不需要身高體重信息、參加奧運會卻關注身高體重,相親時不會關注身份證信息,但卻關注你的工作、年齡等。

1.4 落地經驗

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

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

  • 運營人員:運營人員要配置模型的各種規則,但是頻次相對較低。

  • 用户:用户會使用運營人員配置的規則,使用頻次較高。

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

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

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

1.5 小結

  1. 領域就是有一個範圍,在這個範圍內有不同的角色,每個角色都有該角色應該具備的領域知識,各角色之間通過自己掌握的知識完成彼此協作,完成一些領域活動,產生一些領域事件,最終完成領域職責。
  2. 劃分領域的依據就是領域職責(目標)、領域關注點、完成職責需要的角色、角色需要的知識、角色需要執行的活動。
  3. 事件風暴的過程也是識別領域活動、領域職責、領域角色、領域事件、領域知識的過程。

二、實體

2.1 前言

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

2.2 什麼是實體

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

  • 身份標識

    • 通用類型:ID值沒有業務含義,唯一即可。
    • 領域類型:通常與各個界限上下文的實體對象有關。
  • 屬性:説明主體的靜態特徵,並持有數據與狀態。可以劃分為原子屬性和組合屬性。劃分的依據是:該屬性是否存在約束規則、組合因子或屬於自己的領域行為。

    • 原子屬性:
      • name
    • 組合屬性:
      • price(num, unit);
      • 組合屬性是一種很好的特質,當一個實體有了一些組合屬性後, 一些細小的概念 對應的職責(基本校驗、計算)將由各自的屬性進行負責,而實體更關注自身概念。
  • 領域行為

    • 變更狀態的領域行為:實體對象是允許調用者改變狀態的,這樣就產生了變更狀態的領域行為(方法名上不建議用set/get, 而是更具有業務含義的方法名,這樣更具有領域邏輯(加強))
    • 自給自足的領域行為:對象只操作了自己的屬性,而不依賴於外部屬性。(如校驗一份外賣的總金額、總數量 與外賣中各個單品的關係)
    • 互為協作的領域行為:需要調用者提供必要的信息(一般通過方法參數傳入),這樣就形成了領域對象之間互為協作的領域行為。 增刪改查。

2.3 構建實體的依據

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

  • 相同:在 mvc 的開發模式中,開發者通過閲讀dao 層的表結構,就能瞭解到整個系統大致的架構與作用。同樣的,在 DDD 中開發者通過閲讀實體的屬性,就能瞭解到整個系統大致的架構與作用。
  • 不同:在 DDD 中,一個實體的屬性可能只由一張表組成,也可能由多張表組成,也可能是由mysql 和 redis 共同組成,在實體所在的領域中,我們並不關心它的底層(數據層)是如何實現的,我們只關心這個實體。

舉個例子

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

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相同才是同一本書;但我們判斷一個位置,當經緯度相同的時候就是同一個位置。這個時候圖書就是定義為實體,座標定義為值對象。

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

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

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

—— END——

推薦閲讀

百度工程師帶你玩轉正則

Diffie-Hellman密鑰協商算法探究

貼吧低代碼高性能規則引擎設計

淺談權限系統在多利熊業務應用

分佈式系統關鍵路徑延遲分析實踐

百度工程師教你玩轉設計模式(裝飾器模式)