導讀:你還在用面向?qū)ο蟮恼Z言寫面向過程的代碼嗎凌唬?你是否正在被復雜的業(yè)務邏輯折磨并齐?是否有時覺得應用開發(fā)沒意思、沒挑戰(zhàn)法瑟、技術(shù)含量低冀膝?其實,應用開發(fā)一點都不簡單霎挟,也不無聊窝剖,業(yè)務的變化比底層基礎實施的變化要多得多,封裝這些變化需要很好的業(yè)務理解力酥夭,抽象能力和建模能力赐纱。
今天我們邀請阿里高級技術(shù)專家張建飛,一起來聊聊為什么需要領域建模熬北,什么是好的模型疙描,又該如何搭建。
為什么要領域建模讶隐?
軟件的世界里沒有銀彈起胰,是用事務腳本還是領域模型沒有對錯之分,關(guān)鍵看是否合適巫延。實際上效五,CQRS就是對事務腳本和領域模型兩種模式的綜合,因為對于Query和報表的場景炉峰,使用領域模型往往會把簡單的事情弄復雜畏妖,此時完全可以用奧卡姆剃刀把領域?qū)犹甑簦苯釉L問Infrastructure疼阔。我個人也是堅決反對過度設計的戒劫,因此對于簡單業(yè)務場景,我強力建議還是使用事務腳本婆廊,其優(yōu)點是簡單迅细、直觀、易上手否彩。但對于復雜的業(yè)務場景疯攒,你再這么玩就不行了,因為一旦業(yè)務變得復雜列荔,事務腳本就很難應對敬尺,容易造成代碼的“一鍋粥”枚尼,系統(tǒng)的腐化速度和復雜性呈指數(shù)級上升。
目前比較有效的治理辦法就是領域建模,因為領域模型是面向?qū)ο蟮模诜庋b業(yè)務邏輯的同時项鬼,提升了對象的內(nèi)聚性和重用性,因為使用了通用語言(Ubiquitous Language)盯质,使得隱藏的業(yè)務邏輯得到顯性化表達,使得復雜性治理成為可能概而。talk is cheap呼巷,直接上一個銀行轉(zhuǎn)賬的例子,對事務腳本和領域模型進行比較赎瑰,孰優(yōu)孰劣一目了然王悍。
銀行轉(zhuǎn)賬事務腳本實現(xiàn)
在事務腳本的實現(xiàn)中,關(guān)于在兩個賬號之間轉(zhuǎn)賬的領域業(yè)務邏輯都被寫在了MoneyTransferService的實現(xiàn)里面了餐曼,而Account僅僅是getters和setters的數(shù)據(jù)結(jié)構(gòu)压储,也就是我們說的貧血模式。
上面的代碼大家看起來應該比較眼熟源譬,因為目前大部分系統(tǒng)都是這么寫的集惋。需求評審完,工程師畫幾張UML圖完成設計踩娘,就開始向上面這樣懟業(yè)務代碼了刮刑,這樣寫基本不用太費腦,完全是面向過程的代碼風格养渴。有些同學可能會說为朋,我這樣寫也可以實現(xiàn)系統(tǒng)功能啊,還是那句話“just because you can, doesn't mean you should”厚脉。說句不好聽的,正是有這么多“沒有追求”胶惰、“不求上進”的碼農(nóng)才造成了應用系統(tǒng)的混亂傻工、敗壞了應用開發(fā)的名聲。這也是為什么很多應用開發(fā)工程師覺得工作沒意思孵滞,技術(shù)含量低中捆,覺得整天就是寫if-else的業(yè)務邏輯代碼,系統(tǒng)又爛坊饶,工作繁瑣泄伪、無聊、沒有成長匿级、沒有成就感蟋滴,所以轉(zhuǎn)向去做中間件啊染厅,去寫JDK啊,覺得那個NB津函。
實際上肖粮,應用開發(fā)一點都不簡單也不無聊,業(yè)務的變化比底層Infrastructure的變化要多得多尔苦,解決的難度也絲毫不比寫底層代碼容易涩馆,只是很多人選擇了用無聊的方式去做。其實我們是有辦法做的更優(yōu)雅的允坚,這種優(yōu)雅的方式就是領域建模魂那,唯有掌握了這種優(yōu)雅你才能實現(xiàn)從工程師向應用架構(gòu)的轉(zhuǎn)型。同樣的業(yè)務邏輯稠项,接下來就讓我們看一下用DDD是怎么做的涯雅。
銀行轉(zhuǎn)賬領域模型實現(xiàn)
如果用DDD的方式實現(xiàn),Account實體除了賬號屬性之外皿渗,還包含了行為和業(yè)務邏輯斩芭,比如debit( )和credit( )方法。
而且透支策略OverdraftPolicy也不僅僅是一個Enum了乐疆,而是被抽象成包含了業(yè)務規(guī)則并采用了策略模式的對象划乖。
而Domain Service只需要調(diào)用Domain Entity對象完成業(yè)務邏輯即可。
通過上面的DDD重構(gòu)后挤土,原來在事務腳本中的邏輯琴庵,被分散到Domain Service,Domain Entity和OverdraftPolicy三個滿足SOLID的對象中仰美,在繼續(xù)閱讀之前迷殿,我建議可以自己先體會一下DDD的好處。
領域建模的好處
面向?qū)ο?/b>
封裝:Account的相關(guān)操作都封裝在Account Entity上咖杂,提高了內(nèi)聚性和可重用性庆寺。
多態(tài):采用策略模式的OverdraftPolicy(多態(tài)的典型應用)提高了代碼的可擴展性。
業(yè)務語義顯性化
通用語言:“一個團隊诉字,一種語言”懦尝,將模型作為語言的支柱。確保團隊在內(nèi)部的所有交流中壤圃,代碼中陵霉,畫圖,寫東西伍绳,特別是講話的時候都要使用這種語言踊挠。例如賬號,轉(zhuǎn)賬冲杀,透支策略效床,這些都是非常重要的領域概念睹酌,如果這些命名都和我們?nèi)粘S懻撘约癙RD中的描述保持一致,將會極大提升代碼的可讀性扁凛,減少認知成本忍疾。
顯性化:就是將隱式的業(yè)務邏輯從一推if-else里面抽取出來,用通用語言去命名谨朝、去寫代碼卤妒、去擴展,讓其變成顯示概念字币,比如“透支策略”這個重要的業(yè)務概念则披,按照事務腳本的寫法,其含義完全淹沒在代碼邏輯中沒有突顯出來洗出,看代碼的人自然也是一臉懵逼士复,而領域模型里面將其用策略模式抽象出來,不僅提高了代碼的可讀性翩活,可擴展性也好了很多阱洪。
如何進行領域建模?
建模方法
領域建模這個話題太大菠镇,關(guān)于此的長篇大論和書籍也很多冗荸,比如什么通過語法和句法深入分析法,在我看來這些方法論有些繁瑣了利耍。好的模型應該是建立在對業(yè)務深入理解的基礎上蚌本,如果業(yè)務理解不到位,你再怎么分析句子也不可能產(chǎn)出好的模型隘梨。就我自己的經(jīng)驗而言程癌,建模也是一個不斷迭代的過程,所以一開始可以簡單點來轴猎,就采用兩步建模法抓住一些核心概念嵌莉,然后寫一些代碼驗證一下run一下,看看順不順捻脖,如果很順滑烦秩,說明沒毛病,否則就要看看是不是需要調(diào)整一下模型郎仆,隨著項目的進行和對業(yè)務理解的不斷深入,這種迭代將持續(xù)進行兜蠕。
那什么是兩步建模法呢扰肌?也就是只需要兩個步驟就能建模了,首先從User Story找名詞和動詞熊杨,然后用UML類圖畫出領域模型曙旭。是不是很簡約盗舰?簡約并不意味著簡單,對于業(yè)務架構(gòu)師和系統(tǒng)分析師來說桂躏,見功力的地方往往就在于此钻趋。
舉個栗子,比如讓你設計一個中介系統(tǒng)剂习,一個典型的User Story可能是“小明去找工作蛮位,中介說你留個電話,有工作機會我會通知你”鳞绕,這里面的關(guān)鍵名詞很可能就是我們需要的領域?qū)ο笫剩∶魇乔舐氄撸娫捠乔舐氄叩膶傩悦呛危薪榘酥薪楣咎呀梗薪閱T工兩個關(guān)鍵對象;工作機會肯定也是關(guān)鍵領域?qū)ο笤┲瘢煌ㄖ@個動詞暗示我們這里用觀察者模式會比較合適拂封。然后再梳理一下領域?qū)ο笾g的關(guān)系,一個求職者可以應聘多個工作機會鹦蠕,一個工作機會也可以被多個求職者應聘冒签,M2M的關(guān)系,中介公司可以包含多個員工片部,O2M的關(guān)系镣衡。對于這樣簡單的場景,這個建模就差不多了档悠。
當然我們的業(yè)務場景往往比這個要復雜廊鸥,而且不是所有的名詞都是領域?qū)ο笠部赡苁菍傩裕膊皇撬械膭釉~都是方法也可能是領域?qū)ο笙剿砸唧w問題具體對待惰说,這個對待的過程需要我們有很好的業(yè)務理解力,抽象能力以及建模的經(jīng)驗(知道為什么公司的job model里那么強調(diào)技術(shù)人員的業(yè)務理解力和抽象能力了吧)缘回,比如通常情況下吆视,價格和庫存只是訂單和商品的一個屬性,但是在阿里系電商業(yè)務場景下酥宴,價格計算和庫存扣減的復雜程度可以讓你懷疑人生啦吧,因此作為電商中臺,把價格和庫存單獨當成一個域(Domain)去對待是很必要的拙寡。
另外授滓,建模不是一個一次性的工作,往往隨著業(yè)務的變化以及我們對業(yè)務的理解越來越深入才能看清系統(tǒng)的全貌,所以迭代重構(gòu)是免不了的般堆,也就是要Agile Modelling在孝。
模型統(tǒng)一和模型演化
建模的過程很像盲人摸象,不同背景人用不同的視角看同一個東西淮摔,其理解也是不一樣的私沮。比如兩個盲人都摸到大象鼻子,一個人認為是像蛇(活的能動)和橙,而另一個人認為像消防水管(可以噴水)仔燕,那么他們將很難集成。雙方都無法接受對方的模型胃碾,因為那不符合自己的體驗涨享。事實上,他們需要一個新的抽象仆百,這個抽象需要把蛇的“活著的特性”與消防水管的“噴水功能”合并到一起厕隧,而這個抽象還應該排除先前兩個模型中一些不確切的含義和屬性,比如毒牙俄周,或者卷起來放到消防車上去的行為吁讨。這就是模型的統(tǒng)一。
世界上唯一不變的就是變化峦朗,模型和代碼一樣也需要不斷的重構(gòu)和精化建丧,每一次的精化之后,開發(fā)人員應該對領域知識有了更加清晰的認識波势。這使得理解上的突破成為可能翎朱,之后,一系列快速的改變得到了更符合用戶需要并更加切合實際的模型尺铣。其功能性及說明性急速增強拴曲,而復雜性卻隨之消失。
這種突破需要我們對業(yè)務有更加深刻的領悟和思考凛忿,然后再加上重構(gòu)的勇氣和能力澈灼,勇氣是項目工期很緊你敢不敢重構(gòu),能力是你有沒有完備的CI保證你的重構(gòu)不破壞現(xiàn)有的業(yè)務邏輯店溢。還是以開篇的轉(zhuǎn)賬來舉個例子叁熔,假如轉(zhuǎn)賬業(yè)務開始變的復雜,要支持現(xiàn)金床牧,信用卡荣回,支付寶,比特幣等多種通道戈咳,且沒種通道的約束不一樣心软,還要支持一對多的轉(zhuǎn)賬革砸。那么你還是用一個transfer(fromAccount, toAccount)就不合適了,可能需要抽象出一個專門的領域?qū)ο骉ransaction糯累,這樣才能更好的表達業(yè)務,其演化過程如下:
領域服務
什么是領域服務册踩?
有些領域中的動作泳姐,它們是一些動詞,看上去卻不屬于任何對象暂吉。它們代表了領域中的一個重要的行為胖秒,所以不能忽略它們或者簡單地把它們合并到某個實體或者值對象中。當這樣的行為從領域中被識別出來時慕的,最佳實踐是將它聲明成一個服務阎肝。這樣的對象不再擁有內(nèi)置的狀態(tài)。它的作用僅僅是為領域提供相應的功能肮街。Service往往是以一個活動來命名风题,而不是Entity來命名。例如開篇轉(zhuǎn)賬的例子嫉父,轉(zhuǎn)賬(transfer)這個行為是一個非常重要的領域概念沛硅,但是它是發(fā)生在兩個賬號之間的,歸屬于賬號Entity并不合適绕辖,因為一個賬號Entity沒有必要去關(guān)聯(lián)他需要轉(zhuǎn)賬的賬號Entity摇肌,這種情況下,使用MoneyTransferDomainService就比較合適了仪际。
識別領域服務围小,主要看它是否滿足以下三個特征:
1. 服務執(zhí)行的操作代表了一個領域概念,這個領域概念無法自然地隸屬于一個實體或者值對象树碱。
2. 被執(zhí)行的操作涉及到領域中的其他的對象肯适。
3. 操作是無狀態(tài)的。
應用服務和領域服務如何劃分赴恨?
在領域建模中疹娶,我們一般將系統(tǒng)劃分三個大的層次,即應用層(Application Layer)伦连,領域?qū)樱―omain Layer)和基礎實施層(Infrastructure Layer)雨饺,關(guān)于這三個層次的詳細內(nèi)容可以參考我的另一篇SOFA框架的分層設計』蟠荆可以看到在App層和Domain層都有服務(Service)额港,這兩個Service如何劃分呢,什么樣的功能應該放在應用層歧焦,什么樣的功能應該放在領域?qū)幽兀?/p>
決定一個服務(Service)應該歸屬于哪一層是很困難的移斩。如果所執(zhí)行的操作概念上屬于應用層肚医,那么服務就應該放到這個層。如果操作是關(guān)于領域?qū)ο蟮南虼桑掖_實是與領域有關(guān)的肠套、為領域的需要服務,那么它就應該屬于領域?qū)印?/b>總的來說猖任,涉及到重要領域概念的行為應該放在Domain層你稚,而其它非領域邏輯的技術(shù)代碼放在App層,例如參數(shù)的解析朱躺,上下文的組裝刁赖,調(diào)用領域服務,消息發(fā)送等长搀。還是銀行轉(zhuǎn)賬的case為例宇弛,下圖給出了劃分的建議:
業(yè)務可視化和可配置化
好的領域建模可以降低應用的復雜性源请,而可視化和可配置化主要是幫助大家(主要是非技術(shù)人員枪芒,比如產(chǎn)品,業(yè)務和客戶)直觀地了解系統(tǒng)和配置系統(tǒng)巢钓,提供了一種“code free”的解決方案病苗,也是SaaS軟件的主要賣點。要注意的是可視化和可配置化難免會給系統(tǒng)增加額外的復雜度症汹,必須慎之又慎硫朦,最好是能使可視化和配置化的邏輯與業(yè)務邏輯盡量少的耦合,否則破壞了原有的架構(gòu)背镇,把事情搞的更復雜就得不償失了咬展。
在
可擴展設計中,我已經(jīng)介紹了我們SOFA架構(gòu)是如何通過擴展點的設計來支撐不同業(yè)務差異化的需求的瞒斩,那么可否更進一步破婆,我們將領域的行為(也叫能力)和擴展點用可視化的方式呈現(xiàn)出來,并對于一些不需要編碼實現(xiàn)的擴展點用配置的方式去完成呢胸囱。當然是可以的祷舀,比如還是開篇轉(zhuǎn)賬的例子,對于透支策略OverdraftPolicy這個業(yè)務擴展點烹笔,新來一個業(yè)務說透支額度不能超過1000裳扯,我們可以完全結(jié)合規(guī)則引擎進行配置化完成,而不需要編碼谤职。
所以我能想到的一種還比較優(yōu)雅的方式饰豺,是通過Annotation注解的方式對領域能力和擴展點進行標注,然后在系統(tǒng)bootstrap階段允蜈,通過代碼掃描的方式冤吨,將這些能力點和擴展點收集起來上傳到中心服務器蒿柳,然后再通過GUI的方式呈現(xiàn)出來,從而做到業(yè)務的可視化和可配置化漩蟆。大概的示意圖如下:
有同學可能會問流程要不要可視化垒探,這里要分清楚兩個概念,業(yè)務邏輯流和工作流怠李,很多同學混淆了這兩個概念叛复。業(yè)務邏輯流是響應一次用戶請求的業(yè)務處理過程,其本身就是業(yè)務邏輯扔仓,對其編排和可視化的意義并不是很大,無外乎只是把代碼邏輯可視化了咖耘,在我們的SOFA框架中翘簇,是通過擴展點和策略模式來處理業(yè)務的分支情況,而我看到我們阿里很多的內(nèi)部系統(tǒng)將這種響應一次用戶請求的業(yè)務邏輯用很重的工作流引擎來做儿倒,美其名曰流程可編排版保,實質(zhì)上往往是把簡單的事情復雜化了。而工作流是指完成一項任務所需要不同節(jié)點的連接夫否,節(jié)點主要分為自動節(jié)點和人工節(jié)點彻犁,其中每個人工節(jié)點都需要用戶的參與,也就是響應一次用戶的請求凰慈,比如審批流程中的經(jīng)理審批節(jié)點汞幢,CRM銷售過程的業(yè)務員的處理節(jié)點等等。
此時可以考慮使用工作流引擎微谓,特別是當你的系統(tǒng)需要讓用戶自定義流程的時候森篷,那就不得不使用可視化和可配置的工作流引擎了,除此之外豺型,最好不要自找麻煩仲智。當然也不排除有用的特別合適的案例,只是我還沒看見姻氨,如果有看見的同學也請告訴我一聲钓辆,一起交流學習。
關(guān)注「技術(shù)邊城」? ?把握前沿技術(shù)脈搏