轉(zhuǎn)載:http://www.reibang.com/p/9f34b9962751
你還在用面向?qū)ο蟮恼Z(yǔ)言寫(xiě)面向過(guò)程的代碼嗎?你是否正在被復(fù)雜的業(yè)務(wù)邏輯折磨姆蘸?是否有時(shí)覺(jué)得應(yīng)用開(kāi)發(fā)沒(méi)意思、沒(méi)挑戰(zhàn)芙委、技術(shù)含量低逞敷?其實(shí),應(yīng)用開(kāi)發(fā)一點(diǎn)都不簡(jiǎn)單灌侣,也不無(wú)聊推捐,業(yè)務(wù)的變化比底層基礎(chǔ)實(shí)施的變化要多得多,封裝這些變化需要很好的業(yè)務(wù)理解力侧啼,抽象能力和建模能力牛柒。
今天我們邀請(qǐng)阿里高級(jí)技術(shù)專(zhuān)家張建飛,一起來(lái)聊聊為什么需要領(lǐng)域建模痊乾,什么是好的模型皮壁,又該如何搭建。
為什么要領(lǐng)域建模哪审?
軟件的世界里沒(méi)有銀彈蛾魄,是用事務(wù)腳本還是領(lǐng)域模型沒(méi)有對(duì)錯(cuò)之分,關(guān)鍵看是否合適湿滓。實(shí)際上滴须,CQRS就是對(duì)事務(wù)腳本和領(lǐng)域模型兩種模式的綜合,因?yàn)閷?duì)于Query和報(bào)表的場(chǎng)景叽奥,使用領(lǐng)域模型往往會(huì)把簡(jiǎn)單的事情弄復(fù)雜扔水,此時(shí)完全可以用奧卡姆剃刀把領(lǐng)域?qū)犹甑簦苯釉L問(wèn)Infrastructure朝氓。我個(gè)人也是堅(jiān)決反對(duì)過(guò)度設(shè)計(jì)的魔市,因此對(duì)于簡(jiǎn)單業(yè)務(wù)場(chǎng)景,我強(qiáng)力建議還是使用事務(wù)腳本赵哲,其優(yōu)點(diǎn)是簡(jiǎn)單嘹狞、直觀、易上手誓竿。但對(duì)于復(fù)雜的業(yè)務(wù)場(chǎng)景磅网,你再這么玩就不行了,因?yàn)橐坏I(yè)務(wù)變得復(fù)雜筷屡,事務(wù)腳本就很難應(yīng)對(duì)涧偷,容易造成代碼的“一鍋粥”簸喂,系統(tǒng)的腐化速度和復(fù)雜性呈指數(shù)級(jí)上升。
目前比較有效的治理辦法就是領(lǐng)域建模燎潮,因?yàn)轭I(lǐng)域模型是面向?qū)ο蟮挠黯诜庋b業(yè)務(wù)邏輯的同時(shí),提升了對(duì)象的內(nèi)聚性和重用性确封,因?yàn)槭褂昧送ㄓ谜Z(yǔ)言(Ubiquitous Language)除呵,使得隱藏的業(yè)務(wù)邏輯得到顯性化表達(dá),使得復(fù)雜性治理成為可能爪喘。talk is cheap颜曾,直接上一個(gè)銀行轉(zhuǎn)賬的例子,對(duì)事務(wù)腳本和領(lǐng)域模型進(jìn)行比較秉剑,孰優(yōu)孰劣一目了然泛豪。
銀行轉(zhuǎn)賬事務(wù)腳本實(shí)現(xiàn)
在事務(wù)腳本的實(shí)現(xiàn)中,關(guān)于在兩個(gè)賬號(hào)之間轉(zhuǎn)賬的領(lǐng)域業(yè)務(wù)邏輯都被寫(xiě)在了MoneyTransferService的實(shí)現(xiàn)里面了侦鹏,而Account僅僅是getters和setters的數(shù)據(jù)結(jié)構(gòu)诡曙,也就是我們說(shuō)的貧血模式。
上面的代碼大家看起來(lái)應(yīng)該比較眼熟略水,因?yàn)槟壳按蟛糠窒到y(tǒng)都是這么寫(xiě)的价卤。需求評(píng)審?fù)辏こ處煯?huà)幾張UML圖完成設(shè)計(jì)渊涝,就開(kāi)始向上面這樣懟業(yè)務(wù)代碼了荠雕,這樣寫(xiě)基本不用太費(fèi)腦,完全是面向過(guò)程的代碼風(fēng)格驶赏。有些同學(xué)可能會(huì)說(shuō),我這樣寫(xiě)也可以實(shí)現(xiàn)系統(tǒng)功能啊既鞠,還是那句話“just because you can, doesn't mean you should”煤傍。說(shuō)句不好聽(tīng)的,正是有這么多“沒(méi)有追求”嘱蛋、“不求上進(jìn)”的碼農(nóng)才造成了應(yīng)用系統(tǒng)的混亂蚯姆、敗壞了應(yīng)用開(kāi)發(fā)的名聲。這也是為什么很多應(yīng)用開(kāi)發(fā)工程師覺(jué)得工作沒(méi)意思洒敏,技術(shù)含量低龄恋,覺(jué)得整天就是寫(xiě)if-else的業(yè)務(wù)邏輯代碼,系統(tǒng)又爛凶伙,工作繁瑣郭毕、無(wú)聊、沒(méi)有成長(zhǎng)函荣、沒(méi)有成就感显押,所以轉(zhuǎn)向去做中間件啊扳肛,去寫(xiě)JDK啊,覺(jué)得那個(gè)NB乘碑。
實(shí)際上挖息,應(yīng)用開(kāi)發(fā)一點(diǎn)都不簡(jiǎn)單也不無(wú)聊,業(yè)務(wù)的變化比底層Infrastructure的變化要多得多兽肤,解決的難度也絲毫不比寫(xiě)底層代碼容易套腹,只是很多人選擇了用無(wú)聊的方式去做。其實(shí)我們是有辦法做的更優(yōu)雅的资铡,這種優(yōu)雅的方式就是領(lǐng)域建模电禀,唯有掌握了這種優(yōu)雅你才能實(shí)現(xiàn)從工程師向應(yīng)用架構(gòu)的轉(zhuǎn)型。同樣的業(yè)務(wù)邏輯害驹,接下來(lái)就讓我們看一下用DDD是怎么做的鞭呕。
銀行轉(zhuǎn)賬領(lǐng)域模型實(shí)現(xiàn)
如果用DDD的方式實(shí)現(xiàn),Account實(shí)體除了賬號(hào)屬性之外宛官,還包含了行為和業(yè)務(wù)邏輯葫松,比如debit( )和credit( )方法。
而且透支策略O(shè)verdraftPolicy也不僅僅是一個(gè)Enum了底洗,而是被抽象成包含了業(yè)務(wù)規(guī)則并采用了策略模式的對(duì)象腋么。
而Domain Service只需要調(diào)用Domain Entity對(duì)象完成業(yè)務(wù)邏輯即可。
通過(guò)上面的DDD重構(gòu)后亥揖,原來(lái)在事務(wù)腳本中的邏輯珊擂,被分散到Domain Service,Domain Entity和OverdraftPolicy三個(gè)滿足SOLID的對(duì)象中费变,在繼續(xù)閱讀之前摧扇,我建議可以自己先體會(huì)一下DDD的好處。
領(lǐng)域建模的好處
面向?qū)ο?/strong>
封裝:Account的相關(guān)操作都封裝在Account Entity上挚歧,提高了內(nèi)聚性和可重用性扛稽。
多態(tài):采用策略模式的OverdraftPolicy(多態(tài)的典型應(yīng)用)提高了代碼的可擴(kuò)展性。
業(yè)務(wù)語(yǔ)義顯性化
通用語(yǔ)言:“一個(gè)團(tuán)隊(duì)滑负,一種語(yǔ)言”在张,將模型作為語(yǔ)言的支柱。確保團(tuán)隊(duì)在內(nèi)部的所有交流中矮慕,代碼中帮匾,畫(huà)圖,寫(xiě)東西痴鳄,特別是講話的時(shí)候都要使用這種語(yǔ)言瘟斜。例如賬號(hào),轉(zhuǎn)賬,透支策略哼转,這些都是非常重要的領(lǐng)域概念明未,如果這些命名都和我們?nèi)粘S懻撘约癙RD中的描述保持一致,將會(huì)極大提升代碼的可讀性壹蔓,減少認(rèn)知成本趟妥。
顯性化:就是將隱式的業(yè)務(wù)邏輯從一推if-else里面抽取出來(lái),用通用語(yǔ)言去命名佣蓉、去寫(xiě)代碼披摄、去擴(kuò)展,讓其變成顯示概念勇凭,比如“透支策略”這個(gè)重要的業(yè)務(wù)概念疚膊,按照事務(wù)腳本的寫(xiě)法,其含義完全淹沒(méi)在代碼邏輯中沒(méi)有突顯出來(lái)虾标,看代碼的人自然也是一臉懵逼寓盗,而領(lǐng)域模型里面將其用策略模式抽象出來(lái),不僅提高了代碼的可讀性璧函,可擴(kuò)展性也好了很多傀蚌。
如何進(jìn)行領(lǐng)域建模?
建模方法
領(lǐng)域建模這個(gè)話題太大蘸吓,關(guān)于此的長(zhǎng)篇大論和書(shū)籍也很多善炫,比如什么通過(guò)語(yǔ)法和句法深入分析法,在我看來(lái)這些方法論有些繁瑣了库继。好的模型應(yīng)該是建立在對(duì)業(yè)務(wù)深入理解的基礎(chǔ)上箩艺,如果業(yè)務(wù)理解不到位,你再怎么分析句子也不可能產(chǎn)出好的模型宪萄。就我自己的經(jīng)驗(yàn)而言艺谆,建模也是一個(gè)不斷迭代的過(guò)程,所以一開(kāi)始可以簡(jiǎn)單點(diǎn)來(lái)拜英,就采用兩步建模法抓住一些核心概念静汤,然后寫(xiě)一些代碼驗(yàn)證一下run一下,看看順不順聊记,如果很順滑,說(shuō)明沒(méi)毛病恢暖,否則就要看看是不是需要調(diào)整一下模型排监,隨著項(xiàng)目的進(jìn)行和對(duì)業(yè)務(wù)理解的不斷深入,這種迭代將持續(xù)進(jìn)行杰捂。
那什么是兩步建模法呢舆床?也就是只需要兩個(gè)步驟就能建模了,首先從User Story找名詞和動(dòng)詞,然后用UML類(lèi)圖畫(huà)出領(lǐng)域模型挨队。是不是很簡(jiǎn)約谷暮?簡(jiǎn)約并不意味著簡(jiǎn)單,對(duì)于業(yè)務(wù)架構(gòu)師和系統(tǒng)分析師來(lái)說(shuō)盛垦,見(jiàn)功力的地方往往就在于此湿弦。
舉個(gè)栗子,比如讓你設(shè)計(jì)一個(gè)中介系統(tǒng)腾夯,一個(gè)典型的User Story可能是“小明去找工作颊埃,中介說(shuō)你留個(gè)電話,有工作機(jī)會(huì)我會(huì)通知你”蝶俱,這里面的關(guān)鍵名詞很可能就是我們需要的領(lǐng)域?qū)ο蟀嗬∶魇乔舐氄撸娫捠乔舐氄叩膶傩哉ゴ簦薪榘酥薪楣韭薇辏薪閱T工兩個(gè)關(guān)鍵對(duì)象;工作機(jī)會(huì)肯定也是關(guān)鍵領(lǐng)域?qū)ο蠡撸煌ㄖ@個(gè)動(dòng)詞暗示我們這里用觀察者模式會(huì)比較合適闯割。然后再梳理一下領(lǐng)域?qū)ο笾g的關(guān)系,一個(gè)求職者可以應(yīng)聘多個(gè)工作機(jī)會(huì)浅侨,一個(gè)工作機(jī)會(huì)也可以被多個(gè)求職者應(yīng)聘纽谒,M2M的關(guān)系,中介公司可以包含多個(gè)員工如输,O2M的關(guān)系鼓黔。對(duì)于這樣簡(jiǎn)單的場(chǎng)景,這個(gè)建模就差不多了不见。
當(dāng)然我們的業(yè)務(wù)場(chǎng)景往往比這個(gè)要復(fù)雜澳化,而且不是所有的名詞都是領(lǐng)域?qū)ο笠部赡苁菍傩裕膊皇撬械膭?dòng)詞都是方法也可能是領(lǐng)域?qū)ο笪人保砸唧w問(wèn)題具體對(duì)待缎谷,這個(gè)對(duì)待的過(guò)程需要我們有很好的業(yè)務(wù)理解力,抽象能力以及建模的經(jīng)驗(yàn)(知道為什么公司的job model里那么強(qiáng)調(diào)技術(shù)人員的業(yè)務(wù)理解力和抽象能力了吧)灶似,比如通常情況下列林,價(jià)格和庫(kù)存只是訂單和商品的一個(gè)屬性,但是在阿里系電商業(yè)務(wù)場(chǎng)景下酪惭,價(jià)格計(jì)算和庫(kù)存扣減的復(fù)雜程度可以讓你懷疑人生希痴,因此作為電商中臺(tái),把價(jià)格和庫(kù)存單獨(dú)當(dāng)成一個(gè)域(Domain)去對(duì)待是很必要的春感。
另外砌创,建模不是一個(gè)一次性的工作虏缸,往往隨著業(yè)務(wù)的變化以及我們對(duì)業(yè)務(wù)的理解越來(lái)越深入才能看清系統(tǒng)的全貌,所以迭代重構(gòu)是免不了的嫩实,也就是要Agile Modelling刽辙。
模型統(tǒng)一和模型演化
建模的過(guò)程很像盲人摸象,不同背景人用不同的視角看同一個(gè)東西甲献,其理解也是不一樣的宰缤。比如兩個(gè)盲人都摸到大象鼻子,一個(gè)人認(rèn)為是像蛇(活的能動(dòng))竟纳,而另一個(gè)人認(rèn)為像消防水管(可以噴水)撵溃,那么他們將很難集成。雙方都無(wú)法接受對(duì)方的模型锥累,因?yàn)槟遣环献约旱捏w驗(yàn)缘挑。事實(shí)上,他們需要一個(gè)新的抽象桶略,這個(gè)抽象需要把蛇的“活著的特性”與消防水管的“噴水功能”合并到一起语淘,而這個(gè)抽象還應(yīng)該排除先前兩個(gè)模型中一些不確切的含義和屬性,比如毒牙际歼,或者卷起來(lái)放到消防車(chē)上去的行為惶翻。這就是模型的統(tǒng)一。
世界上唯一不變的就是變化鹅心,模型和代碼一樣也需要不斷的重構(gòu)和精化吕粗,每一次的精化之后,開(kāi)發(fā)人員應(yīng)該對(duì)領(lǐng)域知識(shí)有了更加清晰的認(rèn)識(shí)旭愧。這使得理解上的突破成為可能颅筋,之后欺劳,一系列快速的改變得到了更符合用戶需要并更加切合實(shí)際的模型醋拧。其功能性及說(shuō)明性急速增強(qiáng)注簿,而復(fù)雜性卻隨之消失找爱。
這種突破需要我們對(duì)業(yè)務(wù)有更加深刻的領(lǐng)悟和思考,然后再加上重構(gòu)的勇氣和能力卦停,勇氣是項(xiàng)目工期很緊你敢不敢重構(gòu)壮锻,能力是你有沒(méi)有完備的CI保證你的重構(gòu)不破壞現(xiàn)有的業(yè)務(wù)邏輯宇驾。還是以開(kāi)篇的轉(zhuǎn)賬來(lái)舉個(gè)例子瞳收,假如轉(zhuǎn)賬業(yè)務(wù)開(kāi)始變的復(fù)雜碉京,要支持現(xiàn)金,信用卡螟深,支付寶谐宙,比特幣等多種通道,且沒(méi)種通道的約束不一樣血崭,還要支持一對(duì)多的轉(zhuǎn)賬卧惜。那么你還是用一個(gè)transfer(fromAccount, toAccount)就不合適了,可能需要抽象出一個(gè)專(zhuān)門(mén)的領(lǐng)域?qū)ο骉ransaction夹纫,這樣才能更好的表達(dá)業(yè)務(wù)咽瓷,其演化過(guò)程如下:
什么是領(lǐng)域服務(wù)?
有些領(lǐng)域中的動(dòng)作舰讹,它們是一些動(dòng)詞茅姜,看上去卻不屬于任何對(duì)象。它們代表了領(lǐng)域中的一個(gè)重要的行為月匣,所以不能忽略它們或者簡(jiǎn)單地把它們合并到某個(gè)實(shí)體或者值對(duì)象中钻洒。當(dāng)這樣的行為從領(lǐng)域中被識(shí)別出來(lái)時(shí),最佳實(shí)踐是將它聲明成一個(gè)服務(wù)锄开。這樣的對(duì)象不再擁有內(nèi)置的狀態(tài)素标。它的作用僅僅是為領(lǐng)域提供相應(yīng)的功能。Service往往是以一個(gè)活動(dòng)來(lái)命名萍悴,而不是Entity來(lái)命名头遭。例如開(kāi)篇轉(zhuǎn)賬的例子,轉(zhuǎn)賬(transfer)這個(gè)行為是一個(gè)非常重要的領(lǐng)域概念癣诱,但是它是發(fā)生在兩個(gè)賬號(hào)之間的计维,歸屬于賬號(hào)Entity并不合適,因?yàn)橐粋€(gè)賬號(hào)Entity沒(méi)有必要去關(guān)聯(lián)他需要轉(zhuǎn)賬的賬號(hào)Entity撕予,這種情況下鲫惶,使用MoneyTransferDomainService就比較合適了。
識(shí)別領(lǐng)域服務(wù)实抡,主要看它是否滿足以下三個(gè)特征:
1. 服務(wù)執(zhí)行的操作代表了一個(gè)領(lǐng)域概念欠母,這個(gè)領(lǐng)域概念無(wú)法自然地隸屬于一個(gè)實(shí)體或者值對(duì)象。
2. 被執(zhí)行的操作涉及到領(lǐng)域中的其他的對(duì)象澜术。
3. 操作是無(wú)狀態(tài)的艺蝴。
應(yīng)用服務(wù)和領(lǐng)域服務(wù)如何劃分?
在領(lǐng)域建模中鸟废,我們一般將系統(tǒng)劃分三個(gè)大的層次猜敢,即應(yīng)用層(Application Layer),領(lǐng)域?qū)樱―omain Layer)和基礎(chǔ)實(shí)施層(Infrastructure Layer)盒延,關(guān)于這三個(gè)層次的詳細(xì)內(nèi)容可以參考我的另一篇SOFA框架的分層設(shè)計(jì)缩擂。可以看到在App層和Domain層都有服務(wù)(Service)添寺,這兩個(gè)Service如何劃分呢胯盯,什么樣的功能應(yīng)該放在應(yīng)用層,什么樣的功能應(yīng)該放在領(lǐng)域?qū)幽兀?/p>
決定一個(gè)服務(wù)(Service)應(yīng)該歸屬于哪一層是很困難的计露。如果所執(zhí)行的操作概念上屬于應(yīng)用層博脑,那么服務(wù)就應(yīng)該放到這個(gè)層憎乙。如果操作是關(guān)于領(lǐng)域?qū)ο蟮模掖_實(shí)是與領(lǐng)域有關(guān)的叉趣、為領(lǐng)域的需要服務(wù)泞边,那么它就應(yīng)該屬于領(lǐng)域?qū)印疗杉?偟膩?lái)說(shuō)阵谚,涉及到重要領(lǐng)域概念的行為應(yīng)該放在Domain層,而其它非領(lǐng)域邏輯的技術(shù)代碼放在App層烟具,例如參數(shù)的解析梢什,上下文的組裝,調(diào)用領(lǐng)域服務(wù)朝聋,消息發(fā)送等嗡午。還是銀行轉(zhuǎn)賬的case為例,下圖給出了劃分的建議:
業(yè)務(wù)可視化和可配置化
好的領(lǐng)域建募胶郏可以降低應(yīng)用的復(fù)雜性翼馆,而可視化和可配置化主要是幫助大家(主要是非技術(shù)人員,比如產(chǎn)品金度,業(yè)務(wù)和客戶)直觀地了解系統(tǒng)和配置系統(tǒng)应媚,提供了一種“code free”的解決方案,也是SaaS軟件的主要賣(mài)點(diǎn)猜极。要注意的是可視化和可配置化難免會(huì)給系統(tǒng)增加額外的復(fù)雜度中姜,必須慎之又慎,最好是能使可視化和配置化的邏輯與業(yè)務(wù)邏輯盡量少的耦合跟伏,否則破壞了原有的架構(gòu)丢胚,把事情搞的更復(fù)雜就得不償失了。
在可擴(kuò)展設(shè)計(jì)中受扳,我已經(jīng)介紹了我們SOFA架構(gòu)是如何通過(guò)擴(kuò)展點(diǎn)的設(shè)計(jì)來(lái)支撐不同業(yè)務(wù)差異化的需求的携龟,那么可否更進(jìn)一步,我們將領(lǐng)域的行為(也叫能力)和擴(kuò)展點(diǎn)用可視化的方式呈現(xiàn)出來(lái)勘高,并對(duì)于一些不需要編碼實(shí)現(xiàn)的擴(kuò)展點(diǎn)用配置的方式去完成呢峡蟋。當(dāng)然是可以的,比如還是開(kāi)篇轉(zhuǎn)賬的例子华望,對(duì)于透支策略O(shè)verdraftPolicy這個(gè)業(yè)務(wù)擴(kuò)展點(diǎn)蕊蝗,新來(lái)一個(gè)業(yè)務(wù)說(shuō)透支額度不能超過(guò)1000,我們可以完全結(jié)合規(guī)則引擎進(jìn)行配置化完成赖舟,而不需要編碼蓬戚。
所以我能想到的一種還比較優(yōu)雅的方式,是通過(guò)Annotation注解的方式對(duì)領(lǐng)域能力和擴(kuò)展點(diǎn)進(jìn)行標(biāo)注宾抓,然后在系統(tǒng)bootstrap階段子漩,通過(guò)代碼掃描的方式豫喧,將這些能力點(diǎn)和擴(kuò)展點(diǎn)收集起來(lái)上傳到中心服務(wù)器,然后再通過(guò)GUI的方式呈現(xiàn)出來(lái)幢泼,從而做到業(yè)務(wù)的可視化和可配置化嘿棘。大概的示意圖如下:
有同學(xué)可能會(huì)問(wèn)流程要不要可視化,這里要分清楚兩個(gè)概念旭绒,業(yè)務(wù)邏輯流和工作流,很多同學(xué)混淆了這兩個(gè)概念焦人。業(yè)務(wù)邏輯流是響應(yīng)一次用戶請(qǐng)求的業(yè)務(wù)處理過(guò)程挥吵,其本身就是業(yè)務(wù)邏輯,對(duì)其編排和可視化的意義并不是很大花椭,無(wú)外乎只是把代碼邏輯可視化了忽匈,在我們的SOFA框架中,是通過(guò)擴(kuò)展點(diǎn)和策略模式來(lái)處理業(yè)務(wù)的分支情況矿辽,而我看到我們阿里很多的內(nèi)部系統(tǒng)將這種響應(yīng)一次用戶請(qǐng)求的業(yè)務(wù)邏輯用很重的工作流引擎來(lái)做丹允,美其名曰流程可編排,實(shí)質(zhì)上往往是把簡(jiǎn)單的事情復(fù)雜化了袋倔。而工作流是指完成一項(xiàng)任務(wù)所需要不同節(jié)點(diǎn)的連接雕蔽,節(jié)點(diǎn)主要分為自動(dòng)節(jié)點(diǎn)和人工節(jié)點(diǎn),其中每個(gè)人工節(jié)點(diǎn)都需要用戶的參與宾娜,也就是響應(yīng)一次用戶的請(qǐng)求批狐,比如審批流程中的經(jīng)理審批節(jié)點(diǎn),CRM銷(xiāo)售過(guò)程的業(yè)務(wù)員的處理節(jié)點(diǎn)等等前塔。
此時(shí)可以考慮使用工作流引擎嚣艇,特別是當(dāng)你的系統(tǒng)需要讓用戶自定義流程的時(shí)候,那就不得不使用可視化和可配置的工作流引擎了华弓,除此之外食零,最好不要自找麻煩。當(dāng)然也不排除有用的特別合適的案例寂屏,只是我還沒(méi)看見(jiàn)贰谣,如果有看見(jiàn)的同學(xué)也請(qǐng)告訴我一聲,一起交流學(xué)習(xí)迁霎。