使用函數式語言來創建領域模型

使用函數式語言來創建領域模型

領域模型=代碼=文檔

若是說敏捷軟件開發主張面對面溝通,經過快速迭代的手段,讓有價值的軟件儘早面向市場,從而適應快速變化的需求。
html

那麼DDD則爲敏捷開發過程當中的溝通形式做出了進一步的補充,DDD讓領域模型和代碼以及文檔之間畫上了等號,主張讓代碼成爲團隊之間溝通和交流的途徑。縱觀DDD的全部環節,無一不是在打通領域專家和開發人員之間的溝通和交流,而代碼無疑是最有效,最實時的共享模型。
DDD的精髓在於經過讓開發人員理解領域,進而讓開發人員使用編程語言創建一個跟領域專家腦海中一致的領域模型,使得該領域模型成爲你們共享知識的途徑,這將有效的減小不一樣利益相關者的溝通及交流,確保全部人都在解決同一個問題。
git

領域建模

領域建模是整個DDD環節中最最考驗開發人員功底的一環,不一樣於傳統的數據庫建模技術,開發人員須要有很好的抽象能力,經過恰如其分的編程技術,將領域知識映射到一個代碼模型中。
長期以來OO語言被認爲是領域建模的首選,一些OO的技巧能夠很好的用來抽象領域模型。而函數式語言則被廣泛認爲只能用來作數據處理,科學計算等。本文將爲你們展現如何經過函數式編程語言進行領域建模,本文選用TypeScript編寫實例,TypeScript類型系統徹底知足函數式編程需求,固然本文也適用於其餘擁有靜態類型系統的函數式編程語言。github

TypeScript的類型系統

實際上你只須要知道少許的知識就能夠開始領域建模了,從這個角度來說,實際上函數式類型系統更適合領域建模,從而讓領域模型成爲文檔。typescript

類型

各種編程語言在設計的時候就已經提供了相似string, bool, number等簡單類型(primitive),然而在真實世界裏面,你還須要將這些類型組合成更大的類型,從而來映射現實世界。
在TypeScript中,type關鍵字用來組合更大的類型:數據庫

type Name = {
  firstName: string
  middleName: string
  lastName: string
}

上面類型的用途是顯而易見的,除此以外type還有起別名的用途,不要小瞧這個特性,他能夠幫助你把領域知識記載在你的領域模型中,考慮下面的代碼:編程

const timeToFly = 10

你能一眼看出這句代碼表明的領域知識嗎?也許不能,fly多久?查文檔?No,你應該時刻告訴本身,代碼等於文檔。改進後的代碼以下:session

type Second = number
const timeToFly: Second = 10

Or類型

OO語言沒法建立這種類型,在TypeScript,這種類型被稱爲聯合(Union Types),經過符號|來建立,考慮下面的類型:框架

type Pet = Fish | Bird

PetFish或者是Bird類型。通常來講函數式語言都會有強大的模式匹配能力,來處理這種類型,然而受制於TypesScript沒有模式匹配或者說能力很弱,一般狀況下,會在類型裏面添加一個字符串字面量, 從而來區分不一樣的類型, 在次再也不細說。編程語言

And類型

在Typescript中,這種類型被稱爲交叉類型(Intersection Types),經過符號&來建立,考慮下面的類型:函數式編程

type ABC = A & B & C

表示ABC類型包含全部A、B、C三個類型裏面的屬性。

定義函數類型

在TypeScript中,函數與其餘類型沒什麼區別,也能夠經過type關鍵字來定義,例如:

type Add = (a: number) => (b: number) => number

Add是一個函數,接收兩個類型爲number的類型a和b,返回number。

經過代碼來共享領域知識

type CreditCard = {
  cardNo: string
  firstName: string
  middleName: string
  lastName: string
  contactEmail: Email
  contactPhone: Phone
}

經過前面介紹的知識,咱們很容易就能夠寫出上面的代碼,用來描述CreditCard這種支付方式。注意咱們沒有使用class
但這是一個靠譜的領域模型嗎?若是不靠譜,它的問題在哪裏?
這段代碼最大的問題是他沒有把本該擁有的領域知識記錄在其中,我來試着問你幾個問題:
問:middle name能夠爲空嗎?
答1:不清楚,也許須要查文檔。
答2:也許能夠吧?middle name能夠爲null

爲可空類型建模

在函數式編程語言中,可空類型被定義爲Option ,雖然null在ts中是合法的(注:咱們能夠經過strictNullChecks來強致null檢查),可是在函數式編程語言中,你只能經過Option類型來表達可空類型。
當領域專家告訴你: middle name能夠存在,或者爲空。注意用詞 ,說明咱們能夠經過Union類型來爲可空類型建模。

type Option<T> =  T | null

一個簡單的Option 其實就是一個 類型, 固然你可使用一個更加複雜的 Option實現, 不過不在咱們今天的討論範圍內。通過修改後的代碼變成了這樣:

type CreditCard = {
  cardNo: string
  firstName: string
  middleName: Option<string>
  lastName: string
  contactEmail: Email
  contactPhone: Phone
}

避免基本類型偏執(Primitive Obsession)

問:cardNo能夠用string來表示嗎?若是是,它能夠是任意字符串嗎?firstName能夠是任意長度的字符串嗎?很顯然,你沒法回答上面的問題,源於這個模型並無包含有此類領域知識。
也許在編程語言裏面,cardNo能夠用string表達,可是cardNo在領域模型中,string沒法表達出cardNo的領域知識。
cardNo是一個200打頭的19位字符串,name是一個不超過50位的字符串,這樣的領域信息能夠經過type alias來實現:

type CardNo = string
type Name50 = string
...

有了上面兩個類型,你就有機會經過定義函數的方式,將cardNo業務規則包含在領域模型中。

type GetCardNo = (cardNo: string) => CardNo

若是用戶輸入了一個20位的字符串,函數GetCardNo返回什麼?null?拋出異常?實際上函數式編程語言有比異常更加優雅的Error handling方式, 例如Either Monad或者Railway oriented programming。本文雖然不包含這類話題,但至少目前咱們能夠用Option來表示這個函數簽名:

type GetCardNo = (cardNo: string) => Option<CardNo>

這個函數類型清晰的表達了整個驗證過程,用戶輸入一個字符串, 返回一個CardNo類型,或者空。修改後的領域模型變成了這樣:

type CreditCard = {
 cardNo: Option<CardNo>
 firstName: Name50
 middleName: Option<string>
 lastName: Name50
 contactEmail: Email
 contactPhone: Phone
}

因而,如今的代碼擁有跟多的領域知識,豐富的類型還充當了單元測試的角色,例如,你永遠都不會把一個email賦值給contactPhone,它們不是string, 它們表明不一樣的領域知識。

領域模型的原子性和聚合性

這個領域模型中的三個name能夠分別修改嗎?例如只修改middle name?若是不能夠,如何將這種原子性的修改知識包含在領域模型中?
實際上咱們很容易就能把NameContact兩個類型分離出來並加以組合:

type Name = {
  firstName: Name50
  middleName: Option<string>
  lastName: Name50
}

type Contact = {
  contactEmail: Email
  contactPhone: Phone
}

type CreditCard3 = {
  cardNo: Option<CardNo>
  name: Name
  contact: Contact
}

Make illegal states unrepresentable

在領域建模過程當中,這是一條很是重要的原則,用通俗的話能夠理解爲:你創建的領域模型應該有儘量多的靜態檢查和約束,讓錯誤發生在編譯時,而不是運行時,從而杜絕犯錯誤的機會。其實整個領域建模都是在遵循這個原則,例如上面的Email類型和Phone類型,爲何不用string來表示呢?由於string給與的領域知識不夠,從而容許開發人員有了犯錯誤的機會。
讓咱們最後看一個例子,用來講明這條原則如何被應用在領域建模中。 上面領域模型中有一個contact類型,包含一個Email和Phone屬性。支付成功後,系統能夠經過這兩個屬性給用戶發通知,由此延伸出來這樣一條規則:用戶必須至少填寫一個Email或者一個Phone來接受支付消息。
首先,上面的領域模型是不匹配這條業務規則的,由於Email和Phone類型都是非空類型,意味着這兩個屬性都應該是必填項。
咱們能不能把它倆都改成Option類型呢?

type Contact = {
  contactEmail: Option<Email>
  contactPhone: Option<Phone>
}

顯然也不行,實際上就是違反了Make illegal states unrepresentable, 給與了代碼犯錯的機會,你的領域模型表達出了一種非法的狀態,即Email和Phone均可覺得空,你也許會說個人xxService作了驗證呢,它倆絕對不會同時爲空。對不起,咱們但願咱們的領域模型可以包含這種領域知識,至於xxService,跟領域模型無關。到底可否將這一規則表達在領域模型中嗎?答案是確定的,規則中有一個字,即咱們能夠經過Or類型(union)來表達這種關係:

type OnlyContactEmail = Email 
type OnlyContactPhone = Phone
type BothContactEmailAndPhone = Email & Phone

type Contact = 
  | OnlyContactEmail
  | OnlyContactPhone
  | BothContactEmailAndPhone

結束語

本文旨在經過函數式編程語言來指導領域建模,整個代碼示例中沒有出現類或者子類,更不會出現abstract, bean等關鍵字,衡量一個領域模型的好壞取決於
1)領域模型是否包含了儘量多的領域知識,可否反映領域專家腦海中的業務模型
2)領域模型可否成爲文檔,進而成爲全部人溝通和共享知識的途徑
同時,一些語言,框架的」行話「應該越少越好,例如你在領域模型中建立了一個叫作AbstractContactBase的類,除了增長複雜度,對共享領域模型這一目的幫助甚少。 實際上函數式編程語言的類型系統,不但可以幫助開發者創建一個豐富的領域模型,同時簡單可組合的類型系統,也爲代碼即文檔提供了基礎。不能否認真實世界遠比本文所描述的例子複雜,可是大部分複雜的部分,並不會出如今領域模型中,例如函數式編程中的各類」行話「,他們每每出如今數據請求的validation, 請求第三方,數據轉化,持久化等實現階段。在將來的文章中將會描述整個http請求到領域模型再到輸出過程當中如何經過函數式編程語言來實現。

相關文章
相關標籤/搜索