使用函數(shù)式語言實(shí)踐DDD

長期以來我都在實(shí)踐OOP脚祟,進(jìn)而通過OOP來實(shí)現(xiàn)DDD寓涨,通過面向?qū)ο蟮募记蓙斫⒁粋€領(lǐng)域模型。OO的一些特性在建立領(lǐng)域模型時顯得恰如其分,能否掌握OO的技巧炕檩,對創(chuàng)建領(lǐng)域模型有著至關(guān)重要的作用。

這篇文章為大家介紹一種常見的函數(shù)式架構(gòu)揩悄,特別是如何通過函數(shù)式語言實(shí)現(xiàn)DDD丈积,進(jìn)而利用函數(shù)式組合的特性,創(chuàng)建函數(shù)pipeline惠拭。

軟件架構(gòu)是圍繞著領(lǐng)域模型而做的若干設(shè)計(jì)扩劝,如果按照C4模型的定義,軟件架構(gòu)由下面四個級別的架構(gòu)組成的:

  • “System context”是最高層的架構(gòu),代表著整個系統(tǒng)
  • “Container”是組成”System context”的單元棒呛,通常用來表示可部署的單元聂示,例如一個”API service”, 一個web應(yīng)用程序等
  • “Component”是組成”Container”的基本單元,通常指組若干抽象組件簇秒,是一個”Container”里面的骨架鱼喉,也是本文要重點(diǎn)介紹的架構(gòu)
  • “Code”具體到了代碼級別,通常指實(shí)現(xiàn)某個”Component”應(yīng)該有哪幾個類組成

使用單體應(yīng)用來承載多個限界上下文

領(lǐng)域驅(qū)動設(shè)計(jì)中有一半概念是在討論問題域趋观,并不是一上來就教你如何寫代碼蒲凶,這說明理解一個問題域是復(fù)雜的,看清問題的本質(zhì)是需要時間的拆内。當(dāng)你開始著手劃分限界上下文的時候旋圆,說明你已經(jīng)對需求有了很好的了解。但是經(jīng)驗(yàn)告訴我們麸恍,剛開始你的理解灵巧,往往都不是最終的需求,或者仍然需要多次跟領(lǐng)域?qū)<掖_認(rèn)和交互抹沪,才能得到最終的需求刻肄。

這個時候,如果你一上來就按照限界上下文劃分微服務(wù)融欧,往往可能會步入Microservice Premium敏弃。

要想軟件在一開始就能達(dá)到快速試錯的目的,一上來就做微服務(wù), 會讓步子邁得有點(diǎn)大噪馏。微服務(wù)架構(gòu)帶來了分布式的復(fù)雜性麦到,使得前期生產(chǎn)效率大大降低,另外還存在船大難掉頭的情況欠肾,一旦設(shè)計(jì)出現(xiàn)返工瓶颠,生產(chǎn)效率也會打折扣。當(dāng)然刺桃,這不是絕對的粹淋,如果架構(gòu)師已經(jīng)在該行業(yè)深耕多年,對業(yè)務(wù)更是了如指掌瑟慈,項(xiàng)目一開始就設(shè)計(jì)為微服務(wù)也未嘗不可桃移。

在項(xiàng)目初期,在需求還不是非常明確的時候葛碧,你完全可以創(chuàng)建一個單體應(yīng)用借杰,然后通過不同的模塊或程序集來隔離不同的界限上下文,通過不斷的試錯和快速反饋來調(diào)整你的解決方案吹埠。

一種比較嚴(yán)格的說法是第步,當(dāng)你關(guān)閉其中一個微服務(wù)疮装,如果整個應(yīng)用程序都崩了,其實(shí)你設(shè)計(jì)的不是一個微服務(wù)架構(gòu)粘都,而是一個分布式單體應(yīng)用程序廓推。

代碼結(jié)構(gòu)

在過去的若干年里,我經(jīng)常使用一種叫“Layer architecture”的軟件架構(gòu), 這種架構(gòu)往往把代碼分成若干層:

  • 基礎(chǔ)設(shè)施層:通常用來負(fù)責(zé)跟第三方或者數(shù)據(jù)庫打交道翩隧,用來持久化數(shù)據(jù)或者API請求樊展。
  • 領(lǐng)域?qū)踊蛘邩I(yè)務(wù)邏輯層:用來封裝業(yè)務(wù)邏輯
  • 應(yīng)用程序?qū)樱和ǔJ呛鼙〉囊粚樱脕韰f(xié)調(diào)領(lǐng)域?qū)雍突A(chǔ)設(shè)施層
  • 展現(xiàn)層:用來展現(xiàn)UI或者輸出API結(jié)果

這種架構(gòu)方式是一個自上往下的輸入堆生,最后從下往上輸出結(jié)果的工作流(圖1)

  • 實(shí)際上专缠,當(dāng)我在使用這種方式組織代碼時,遇到最大的挑戰(zhàn)在于:這種分層方式淑仆,把同一個輸入到輸出的的若干部分涝婉,橫向的分散到了若干層中。當(dāng)你需要修改某個API時蔗怠,需要同時修改若干個層墩弯。另外這種組織代碼的方式,往往會讓OO走向混亂寞射,一個名叫OrderApplicationService的類中放滿了各種跟Order相關(guān)的方法渔工,通常對Order的操作有數(shù)十種之多,他們屬于OrderApplicationService嗎桥温?如果屬于引矩,任何一個跟Order相關(guān)操作的參數(shù)變化,都會引起這個類被改動侵浸,這種對類的頻繁修改合理嗎旺韭?
  • 函數(shù)式編程中,更傾向于縱向組織代碼(圖2)通惫,例如一個API操作茂翔,就是一個文件或者模塊混蔼,整個操作自上而下的流程被組織到同一個文件里履腋,這樣做的好處是,針對某個功能的修改惭嚣,只關(guān)注與當(dāng)前工作流相關(guān)的文件即可遵湖。

信任邊界

在問題域里,各種業(yè)務(wù)之間的邊界是模糊的晚吞,限界上下文則是業(yè)務(wù)在解決方案上的映射延旧,是人為劃分的邊界。在邊界里面的內(nèi)容槽地,是可信任和合法的迁沫,相反芦瘾,界限外面的一切輸入,則是非法和不可信任的(圖3)集畅。

這就要求我們在限界上下文的邊界近弟,引入驗(yàn)證邏輯,從而阻止外部輸入挺智,以及驗(yàn)證對外部的輸出祷愉。

常見的驗(yàn)證邏輯如:

  • 輸入DTO,需要轉(zhuǎn)化為領(lǐng)域模型赦颇,用于處理業(yè)務(wù)邏輯
  • 對輸入數(shù)據(jù)的合法性驗(yàn)證二鳄,例如:用戶名不能為空,郵件格式是否正確
  • 對輸出類型的安全性校驗(yàn)媒怯,例如:防止在輸出數(shù)據(jù)里包含用戶密碼等敏感信息

驗(yàn)證邏輯并不是FP獨(dú)有的订讼,不過FP中常常使用Applicative對數(shù)據(jù)進(jìn)行驗(yàn)證,從而收集多個用戶Error扇苞。關(guān)于Applicative, 以后會單獨(dú)寫文章介紹躯嫉。
一旦輸入數(shù)據(jù)突破信任邊界,在領(lǐng)域模型建模的過程中杨拐,你不需要擔(dān)心用戶名是否是空祈餐,郵件格式是否正確等問題。你應(yīng)該專注于使用FP的代數(shù)數(shù)據(jù)類型進(jìn)行領(lǐng)域建模哄陶,請參考我之前寫過一篇使用函數(shù)式語言來建立領(lǐng)域模型—類型組合帆阳。

對輸出的驗(yàn)證則不太一樣,主要關(guān)心對輸出數(shù)據(jù)的安全性保護(hù)屋吨,防止將一些領(lǐng)域模型中的私有屬性輸出到外部世界蜒谤。

通過狀態(tài)機(jī)來處理業(yè)務(wù)邏輯

縱然,通過FP的代數(shù)數(shù)據(jù)類型(Algebraic data type)能夠快速完成領(lǐng)域建模至扰,但是我們知道鳍徽,領(lǐng)域模型不是靜態(tài)的,它是由一些列事件組成的過程敢课。而這種轉(zhuǎn)化過程阶祭,正是領(lǐng)域模型狀態(tài)發(fā)生變化的過程,即狀態(tài)機(jī)(圖4)直秆。

領(lǐng)域模型狀態(tài)轉(zhuǎn)換的過程跟實(shí)現(xiàn)語言無關(guān)濒募,一個設(shè)計(jì)精良的領(lǐng)域模型,就好比一個狀態(tài)機(jī)圾结。例如在買機(jī)票的過程中瑰剃,填寫個人信息,填寫聯(lián)系人筝野,選座晌姚,買保險(xiǎn)和付款的過程粤剧,就是訂單狀態(tài)發(fā)生變化的過程。再比如用戶注冊的過程挥唠,填寫基本信息俊扳,驗(yàn)證郵箱,也是用戶信息狀態(tài)發(fā)生變化的過程猛遍。以O(shè)O為例馋记,我們習(xí)慣于通過增加標(biāo)志位的方式,進(jìn)行領(lǐng)域建模:

type User = {
  name: string
  password: string
  email: Email | null 
  isEmailVerified: boolean //當(dāng)驗(yàn)證完email后設(shè)置為true
  canLogin: boolean //當(dāng)email被驗(yàn)證后方可login
}

業(yè)務(wù)邏輯的實(shí)現(xiàn)過程懊烤,就是填充用戶屬性和修改標(biāo)志位的過程梯醒。然而,這種方式實(shí)際上存在若干問題:

  • 有些屬性在業(yè)務(wù)前期是不需要的腌紧,例如canLogin, 只有驗(yàn)證完email才有效
  • 有些標(biāo)志位實(shí)際上不是單獨(dú)存在的茸习,例如isPhoneVerified就跟phone是緊密相關(guān)的,而這個模型無法反映出來這一信息
  • phone和email被定義為可空類型壁肋,導(dǎo)致使用該模型的地方不得不使用null檢查

通過狀態(tài)機(jī)的機(jī)制号胚,重新考慮用戶注冊過程:(圖5)

  • 按照上面的狀態(tài)重新對用戶建模,得到的模型如下:
type UnVerifiedUser = {
  name: string
  password: string
}

type VerifiedEmailUser = {
  name: string
  password: string
  email: Email
}

type User =
  | UnVerifiedUser
  | VerifiedEmailUser

如果有更多的用戶狀態(tài)浸遗,你還可以持續(xù)添加到User類型中猫胁。

這種通過”|”創(chuàng)建的User類型被稱為在FP中被稱為union類型,也叫product或sum類型, 在TypeScript被稱為Discriminated union跛锌。這時候的User類型弃秆,可以用來在領(lǐng)域模型中實(shí)現(xiàn)領(lǐng)域邏輯,通常這種union類型需要配合模式匹配來完成髓帽,例如修改密碼菠赚,登錄,修改郵件地址等邏輯郑藏,都是針對User類型做模式匹配的過程衡查。關(guān)于模式匹配的用法,在此不再細(xì)說必盖。

這種通過狀態(tài)機(jī)的方式拌牲,實(shí)現(xiàn)業(yè)務(wù)邏輯時有下面幾個好處:

  • 業(yè)務(wù)模型在不同的狀態(tài),提供不同的業(yè)務(wù)能力
  • 模式匹配會強(qiáng)制你處理每種狀態(tài)的行為筑悴,避免遺漏一些邊邊角角的情況
  • 相比于將所有狀態(tài)記錄在同一個模型中们拙,狀態(tài)機(jī)可以幫你梳理整個業(yè)務(wù)狀態(tài)的變化

保持純凈的領(lǐng)域模型

函數(shù)式編程的一個主要目標(biāo)就是讓代碼有預(yù)測性,通過函數(shù)簽名理解函數(shù)的用途阁吝。

為了達(dá)到這個目的,函數(shù)式語言設(shè)計(jì)了若干特性械拍,例如不可變的數(shù)據(jù)結(jié)構(gòu)突勇,還有各類Monad來避免副作用装盯。

在DDD實(shí)踐中,應(yīng)該避免I/O相關(guān)的代碼出現(xiàn)Domain中甲馋。例如讀寫數(shù)據(jù)庫埂奈,調(diào)用第三方系統(tǒng)的API等相關(guān)代碼,需要把這類具有副作用的代碼推到Domain的外圍定躏。

如果需要做的更好账磺,那就必須使用CQRS加Event Sourcing。我在之前一篇文章提到過這個觀點(diǎn)痊远,不過部分讀者沒有理解其中的意思垮抗,我在這里再做一些說明。

首先碧聪,CQRS不僅僅是為了讀寫分離冒版,從而提高讀寫性能。讀模型和寫模型(領(lǐng)域模型)的分離意味著職責(zé)也是分離的逞姿,從而在設(shè)計(jì)領(lǐng)域模型的時候辞嗡,打消對查詢性能的考慮,有助于設(shè)計(jì)出純凈的領(lǐng)域模型滞造。

當(dāng)然僅靠CQRS還是不夠的续室,有些時候任然無法完全脫離數(shù)據(jù)庫的考慮,因?yàn)轭I(lǐng)域模型始終是要持久化在數(shù)據(jù)庫里谒养,你就要考慮數(shù)據(jù)庫相關(guān)的約束猎贴,例如主外鍵,如何建表蝴光,如何高效存儲一個列表等她渴。而持久化一個Event則完全擺脫了數(shù)據(jù)庫技術(shù),因?yàn)橐粋€Event就是一個json, 只有這樣才能設(shè)計(jì)出理想的領(lǐng)域模型蔑祟。當(dāng)然引入CQRS和ES在項(xiàng)目初期成本略高趁耗,不再詳細(xì)描述。

通過Monad創(chuàng)建pipeline

以API為例疆虚,一個完整的用戶請求就是一個Pipeline(圖6)苛败。假設(shè)每一步都是有若干個函數(shù)組成,我們能夠?qū)⑺麄兘M合到一起嗎径簿?答案是很難罢屈,主要原因如下:

  • 每一步的若干個函數(shù)簽名很難保持一致,導(dǎo)致compose這樣的函數(shù)無法正常工作
  • 部分I/O相關(guān)的函數(shù)可能是異步的篇亭,領(lǐng)域模型中的代碼大多是同步的缠捌,很難將他們組合在一起
  • 在函數(shù)式編程中,通常不會通過throw的方式處理異常译蒂,一方面異常也是一種副作用曼月,另一方面谊却,異常讓函數(shù)簽名不再完整。如何把每一步的異常帶到最外面也成了問題哑芹。

而解決這一切的手段就是Monad, 簡而言之炎辨,Monad是一種抽象方式,能夠?qū)onadic風(fēng)格的函數(shù)連接起來聪姿。什么又是monadic? 簡單來說這是一種接收普通類型碴萧,返回某種lift類型(泛型)的函數(shù)。例如通過IO, Task, Either相關(guān)的Monad來解決此類問題末购。具體內(nèi)容請關(guān)注本人的函數(shù)式系列博客破喻。

小結(jié)

這篇文章總結(jié)了一些使用函數(shù)式語言實(shí)踐DDD的大致思路,也為函數(shù)式架構(gòu)提供了一些參考招盲。由于篇幅的原因低缩,并沒有介紹到DDD的方方面面,同時曹货,一些實(shí)現(xiàn)細(xì)節(jié)則是點(diǎn)到為止咆繁,例如如何使用Monad《プ眩總體來說玩般,函數(shù)式語言的代數(shù)數(shù)據(jù)類型,以及函數(shù)式的一些思想礼饱,為實(shí)踐領(lǐng)域驅(qū)動設(shè)計(jì)提供了其他的選擇坏为。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市镊绪,隨后出現(xiàn)的幾起案子匀伏,更是在濱河造成了極大的恐慌,老刑警劉巖蝴韭,帶你破解...
    沈念sama閱讀 218,451評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件够颠,死亡現(xiàn)場離奇詭異,居然都是意外死亡榄鉴,警方通過查閱死者的電腦和手機(jī)履磨,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,172評論 3 394
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來庆尘,“玉大人剃诅,你說我怎么就攤上這事∈患桑” “怎么了矛辕?”我有些...
    開封第一講書人閱讀 164,782評論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我如筛,道長堡牡,這世上最難降的妖魔是什么抒抬? 我笑而不...
    開封第一講書人閱讀 58,709評論 1 294
  • 正文 為了忘掉前任杨刨,我火速辦了婚禮,結(jié)果婚禮上擦剑,老公的妹妹穿的比我還像新娘妖胀。我一直安慰自己,他們只是感情好惠勒,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,733評論 6 392
  • 文/花漫 我一把揭開白布赚抡。 她就那樣靜靜地躺著,像睡著了一般纠屋。 火紅的嫁衣襯著肌膚如雪涂臣。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,578評論 1 305
  • 那天售担,我揣著相機(jī)與錄音赁遗,去河邊找鬼。 笑死族铆,一個胖子當(dāng)著我的面吹牛岩四,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播哥攘,決...
    沈念sama閱讀 40,320評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼剖煌,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了逝淹?” 一聲冷哼從身側(cè)響起耕姊,我...
    開封第一講書人閱讀 39,241評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎栅葡,沒想到半個月后茉兰,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,686評論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡妥畏,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,878評論 3 336
  • 正文 我和宋清朗相戀三年邦邦,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片醉蚁。...
    茶點(diǎn)故事閱讀 39,992評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡燃辖,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出网棍,到底是詐尸還是另有隱情黔龟,我是刑警寧澤,帶...
    沈念sama閱讀 35,715評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站氏身,受9級特大地震影響巍棱,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜蛋欣,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,336評論 3 330
  • 文/蒙蒙 一航徙、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧陷虎,春花似錦到踏、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,912評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至凿掂,卻和暖如春伴榔,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背庄萎。 一陣腳步聲響...
    開封第一講書人閱讀 33,040評論 1 270
  • 我被黑心中介騙來泰國打工踪少, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人惨恭。 一個月前我還...
    沈念sama閱讀 48,173評論 3 370
  • 正文 我出身青樓秉馏,卻偏偏與公主長得像,于是被迫代替她去往敵國和親脱羡。 傳聞我的和親對象是個殘疾皇子萝究,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,947評論 2 355

推薦閱讀更多精彩內(nèi)容

  • 領(lǐng)域驅(qū)動設(shè)計(jì)(DDD) 是 Eric Evans 提出的一種軟件設(shè)計(jì)方法和思想,主要解決業(yè)務(wù)系統(tǒng)的設(shè)計(jì)和建模锉罐。DD...
    ThoughtWorks閱讀 1,507評論 1 7
  • 編者按:這篇文章最早撰寫于2014年,作者也是《實(shí)現(xiàn)領(lǐng)域驅(qū)動設(shè)計(jì)》的譯者栽连。幾年過去了侨舆,DDD在坊間依然方興未艾,然...
    ThoughtWorks閱讀 546評論 0 0
  • 在使用 DDD 的思想時熔恢,最讓人迷惑的就是如何組織代碼臭笆,也就是通常所說的系統(tǒng)架構(gòu)的問題秤掌。在前面提到 DDD 可以很...
    ThoughtWorks閱讀 616評論 0 1
  • title: 淺談領(lǐng)域驅(qū)動設(shè)計(jì)DDDdate: 2020-08-19 10:19:19 0. 前言 實(shí)習(xí)期間闻鉴,組長...
    dounine閱讀 858評論 0 2
  • 16宿命:用概率思維提高你的勝算 以前的我是風(fēng)險(xiǎn)厭惡者孟岛,不喜歡去冒險(xiǎn)获黔,但是人生放棄了冒險(xiǎn)在验,也就放棄了無數(shù)的可能。 ...
    yichen大刀閱讀 6,052評論 0 4