使用函數式語言來建立領域模型
領域模型=代碼=文檔
如果說敏捷軟件開發主張面對面溝通,通過快速迭代的手段,讓有價值的軟件盡早面向市場,從而適應快速變化的需求。
那么DDD則為敏捷開發過程中的溝通形式作出了進一步的補充,DDD讓領域模型和代碼以及文檔之間畫上了等號,主張讓代碼成為團隊之間溝通和交流的途徑。縱觀DDD的所有環節,無一不是在打通領域專家和開發人員之間的溝通和交流,而代碼無疑是最有效,最實時的共享模型。
DDD的精髓在於通過讓開發人員理解領域,進而讓開發人員使用編程語言建立一個跟領域專家腦海中一致的領域模型,使得該領域模型成為大家共享知識的途徑,這將有效的減少不同利益相關者的溝通及交流,確保所有人都在解決同一個問題。
領域建模
領域建模是整個DDD環節中最最考驗開發人員功底的一環,不同於傳統的數據庫建模技術,開發人員需要有很好的抽象能力,通過恰如其分的編程技術,將領域知識映射到一個代碼模型中。
長期以來OO語言被認為是領域建模的首選,一些OO的技巧可以很好的用來抽象領域模型。而函數式語言則被普遍認為只能用來做數據處理,科學計算等。本文將為大家展示如何通過函數式編程語言進行領域建模,本文選用TypeScript編寫實例,TypeScript類型系統完全滿足函數式編程需求,當然本文也適用於其他擁有靜態類型系統的函數式編程語言。
TypeScript的類型系統
實際上你只需要知道少量的知識就可以開始領域建模了,從這個角度來講,實際上函數式類型系統更適合領域建模,從而讓領域模型成為文檔。
類型
各類編程語言在設計的時候就已經提供了類似string, bool, number
等簡單類型(primitive),然而在真實世界里面,你還需要將這些類型組合成更大的類型,從而來映射現實世界。
在TypeScript中,type
關鍵字用來組合更大的類型:
type Name = {
firstName: string
middleName: string
lastName: string
}
上面類型的用途是顯而易見的,除此之外type
還有起別名的用途,不要小瞧這個特性,他可以幫助你把領域知識記載在你的領域模型中,考慮下面的代碼:
const timeToFly = 10
你能一眼看出這句代碼代表的領域知識嗎?也許不能,fly多久?查文檔?No,你應該時刻告訴自己,代碼等於文檔。改進后的代碼如下:
type Second = number
const timeToFly: Second = 10
Or類型
OO語言無法創建這種類型,在TypeScript,這種類型被稱為聯合
(Union Types),通過符號|
來創建,考慮下面的類型:
type Pet = Fish | Bird
Pet
是Fish
或者是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
當領域專家告訴你:
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
?如果不可以,如何將這種原子性的修改知識包含在領域模型中?
實際上我們很容易就能把Name
和Contact
兩個類型分離出來並加以組合:
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請求到領域模型再到輸出過程中如何通過函數式編程語言來實現。