CloudNotes之領域建模篇:領域模型簡介

dax.net發表於2015-02-22

CloudNotes領域模型還是相對簡單的,並不一定需要採用面向領域驅動的設計方法來解決CloudNotes的領域問題。但出於以下幾個方面的原因,我還是採用了面向領域驅動的方式來開發CloudNotes:

  1. 領域驅動是企業級應用開發的一種指導性模型,以領域模型作為軟體開發的中心,符合解決問題的基本思路
  2. 現有的企業級應用開發框架對面向領域的開發模式支援得越來越好,如果選用這種方式,可以在CloudNotes中更好地利用這些框架的最新功能,為系統開發尋求新的機遇
  3. 自己對領域驅動設計相對比較熟悉,而且也維護了一套自己研發的DDD開發框架Apworks。在CloudNotes中直接複用該框架,可以大大減小開發投入,縮短開發週期
  4. 溫故而知新,選用DDD來指導CloudNotes開發,可以獲取到有關DDD的更多資訊

接下來,讓我們一起對CloudNote的領域模型作些簡單的瞭解。

基本模型

如果你使用的是Visual Studio 2013/2015旗艦版(Ultimate Edition),那麼在開啟CloudNotes解決方案後,你可以在CloudNotes.Design專案下,找到CloudNotesModel.classdiagram類圖設計檔案:

image

雙擊該檔案,可以開啟CloudNotes的領域模型設計檢視。到目前為止,CloudNotes的領域模型如下:

image

我們們暫且不考慮C# class、aggregate root等這些UML構造型,這些內容我會在接下來的文章中詳細介紹(順帶會介紹Visual Studio對於模型設計與自動化程式碼產生的支援)。從該圖可以看出,CloudNotes的領域模型還是相對簡單的。基本上可以分為三個大部分:筆記、使用者以及客戶包(ClientPackage)。或許你會考慮,是否可以將這些部分看成是DDD的界定上下文(Bounded Context)呢?

界定上下文(Bounded Context)

是的,我們可以考慮將CloudNotes的領域模型劃分為三個界定上下文:

  • 筆記界定上下文:管理系統中所有的筆記內容
  • 使用者認證與授權上下文:管理系統賬戶、角色以及許可權
  • 客戶包管理上下文:管理針對所有客戶端平臺的升級包

從DDD角度看,由於模型被劃分為多個上下文,而且上下文中的子模型也都是高內聚的(界定的),因此有可能會產生概念或語義上的二義性,而這又是通用語言(Ubiquitous Language)所不能容忍的。比如,一個經典的例子就是銀行系統裡“Account” 的概念:一個提供線上服務的銀行系統,簡單地說可以粗略地分為兩個界定上下文:銀行業務以及線上服務。對於銀行業務而言,Account表示客戶的銀行賬戶,而對於線上服務而言,Account則又表示客戶的線上登入賬號,而且一個線上登入賬號下可以承載多個銀行賬戶,好比招商銀行一網通賬號可以有多個銀行卡賬戶和信用卡賬戶等。那麼在做系統設計的時候,遇到Account概念時,如何保證團隊交流的準確性呢?因此,需要在領域模型中有一個能夠解除這種二義性的“元件”,負責幫助團隊人員在整個領域模型的理解與交流上保持一致。這種“元件”就是平時常說的“上下文對映(Context Map)”。有關界定上下文以及上下文對映的詳細介紹,可以參考這篇文章:Strategic Domain Driven Design with Context Mapping

當應用程式所需處理的領域變得很大時,引入界定上下文是很有必要的,從實踐角度考慮,使用界定上下文不僅可以在一個相對封閉的範圍內,使用一個相對較小的子領域模型,而且還可以從一定程度上提升應用程式的效能:比如某個操作只會發生在系統的某個子領域內部,又如較小的領域模型能夠減少系統的處理開銷。在Entity Framework 程式碼先行(Code-First)的開發過程中,可以很好地引入界定上下文的概念,以將複雜的領域模型劃分成多個相對較小的簡單的領域模型,不僅在模型設計還是在效能上,都有著很好的表現。有關這個話題的更多內容,可以參考我之前翻譯的一篇文章:Entity Framework模型在領域驅動設計界定上下文中的應用

CloudNotes的領域模型相對簡單,而且雖然目前後臺採用的是Entity Framework,但使用Entity Framework並不是必須的,今後有可能會換成其它的資料持久化系統(比如MongoDB),因此也沒有按照上面所述的方式應用界定上下文的相關概念。

實體鍵

由於CloudNotes使用了Apworks框架,因此CloudNotes領域模型中的實體都是使用Guid作為實體鍵的。這一問題就要引入對Apworks框架的討論。在Apworks框架中,我們可以看到以下程式碼:

public interface IEntity
{
    /// <summary>
    /// Gets or sets the identifier of the entity.
    /// </summary>
    Guid ID { get; set; }
}

在Apworks中,實體鍵是Guid型別的。有關實體鍵的型別選擇,在Vaughn Vernon的《實現領域驅動設計》一書中,有相關的描述。概括起來,大致有以下幾種情況:

  • 使用者指定實體鍵:這種方式最為直觀:使用者在介面上直接提供某個值,作為實體的鍵值。但通常而言,實體鍵值應該是不可變(immutable)的,於是,當使用者為某個實體選定了實體鍵值後,一般是不允許修改的。但這樣做又顯得跟標準的使用者操作流程不相符:我自己輸入的東西,憑什麼不讓我改?另一種情況就是,使用者可以修改實體鍵的值,但這種修改或許還會帶來更多的影響,例如某個實體鍵發生變化時,由該實體發起的所有事件資料中針對該實體的引用也要發生改變。當然,如果應用程式需要保證實體鍵的可讀性時,我們就不得不設計一套合理的機制,來確保可讀性和可行性不會發生衝突
  • 應用程式產生實體鍵: 很多應用程式會通過應用程式來產生實體鍵,比如通過應用程式開發框架提供的Guid/UUID生成演算法來產生實體鍵。使用Guid不僅簡單而且相對比較高效,更重要的是,它不需要依賴於應用程式以外的任何外部機制,比如持久化系統等。此外,正如《實現領域驅動設計》一書中所述,有些對效能要求很高的系統,會在一個獨立的伺服器上(或者有可能是一個後臺的服務上)快取一定數量的ID值,使用Guid就不會產生因ID伺服器崩潰或重啟而出現ID值重複或不連續的情況,因為Guid的生成程式始終能夠保證ID值的唯一性。另一方面,如果需要通過應用程式來產生實體鍵值,那麼我們就需要完美地解決ID值的同步問題,而且還需要考慮效能問題,更進一步,企業級應用系統往往都是由多個應用程式組成的,是否,以及如何保證實體鍵在這些應用之間的統一,也是一件不容易的事情
  • 藉助持久化機制產生實體鍵:這種情況非常常見,例如ORM系統會藉助關係型資料庫為實體設定鍵值。這也是一種簡單方便的ID值獲取方式,因為可以借用持久化機制來獲得ID值快取的功能,而且還能保證ID值的唯一性。但這往往也並不高效,畢竟需要藉助外部系統,持久化機制的效能表現,也將對應用程式本身的效能造成影響甚至成為效能瓶頸。對於大型分散式企業級應用解決方案而言,也存在ID值的同步和統一的問題,例如應用程式A和B採用了不同的持久化機制,然而卻無法保證原本應該保持一致的實體鍵值
  • 外部界定上下文指定實體鍵:這種方式並不常見,在大型的企業級應用系統解決方案中可以用到,但也並不一定會採用這種方式。它是通過領域事件的方式,在界定上下文之間進行資訊傳遞,比如本地界定上下文(Local Bounded Context)通過訂閱來自外部界定上下文的領域事件,並通過事件中的資訊,以在本地界定上下文中還原實體,於是ID值也一併被還原過來,以便表示當前實體與外部界定上下文中另一物件是同一實體

很顯然,Apworks採用的就是上述Guid的方式,這樣做實現起來比較簡單高效,而且Guid值型別的特性,在絕大多數儲存系統中都能很好地支援。當然它也有一些弊端,比如可讀性差,以及在某些儲存系統中效能並不算太好等。無論如何,就Apworks框架本身而言,應該提供一套實體鍵產生的框架,以便開發人員能夠擴充套件ID值的產生機制,而不應該將ID值型別限定在Guid之上。這在老版本的Apworks中是有類似機制的,只是在考慮到裝箱、拆箱帶來的效能問題後,就在新的版本中,把這種機制給取消了,也就演變到目前的這種情形。今後我還是會繼續改進這部分的設計的。

總結

CloudNotes的領域模型相對簡單,實現上採用了Apworks框架,並借用了Visual Studio Ultimate的Architecture功能,對領域模型進行視覺化設計和自動化程式碼生成。或許在今後領域模型會進一步修改,或者逐漸變大,或許也會由此而採用一些新的技術,併產生新的解決方案。到目前為止,CloudNotes的領域模型規模還是很小的,本系列文章也會首先針對當前的這個模型來進行介紹。

相關文章