深入實(shí)踐DDD(以DSL驅(qū)動(dòng)復(fù)雜軟件開發(fā))_第1頁(yè)
深入實(shí)踐DDD(以DSL驅(qū)動(dòng)復(fù)雜軟件開發(fā))_第2頁(yè)
深入實(shí)踐DDD(以DSL驅(qū)動(dòng)復(fù)雜軟件開發(fā))_第3頁(yè)
深入實(shí)踐DDD(以DSL驅(qū)動(dòng)復(fù)雜軟件開發(fā))_第4頁(yè)
深入實(shí)踐DDD(以DSL驅(qū)動(dòng)復(fù)雜軟件開發(fā))_第5頁(yè)
已閱讀5頁(yè),還剩447頁(yè)未讀, 繼續(xù)免費(fèi)閱讀

下載本文檔

版權(quán)說(shuō)明:本文檔由用戶提供并上傳,收益歸屬內(nèi)容提供方,若內(nèi)容存在侵權(quán),請(qǐng)進(jìn)行舉報(bào)或認(rèn)領(lǐng)

文檔簡(jiǎn)介

深入實(shí)踐DDD以DSL驅(qū)動(dòng)復(fù)雜軟件開發(fā)目錄\h第一部分概念\h第1章DDD的關(guān)鍵概念\h1.1自頂而下、逐步求精\h1.1.1DDD開創(chuàng)全新分析流派\h1.1.2什么是軟件的核心復(fù)雜性\h1.2什么是領(lǐng)域模型\h1.3戰(zhàn)術(shù)層面的關(guān)鍵概念\h1.3.1實(shí)體\h1.3.2值對(duì)象\h1.3.3聚合與聚合根、聚合內(nèi)部實(shí)體\h1.3.4聚合的整體與局部\h1.3.5聚合是數(shù)據(jù)修改的單元\h1.3.6聚合分析是“拆分”的基礎(chǔ)\h1.3.7服務(wù)\h1.4戰(zhàn)略層面的關(guān)鍵概念\h1.4.1限界上下文\h1.4.2限界上下文與微服務(wù)\h1.4.3防腐層\h1.4.4統(tǒng)一語(yǔ)言\h1.5ER模型、OO模型和關(guān)系模型\h1.6概念建模與模型范式\h第2章其他DDD相關(guān)概念\h2.1領(lǐng)域ID\h2.1.1自然鍵與代理鍵\h2.1.2DDD實(shí)體的ID需要被最終用戶看到\h2.1.3什么時(shí)候使用代理鍵\h2.2ID、LocalID與GlobalID\h2.3命令、事件與狀態(tài)\h第3章CQRS與EventSourcing\h3.1命令查詢職責(zé)分離\h3.2事件溯源\h3.3From-Thru模式\h3.3.1示例:ProductPrice\h3.3.2示例:PartyRelationship\h3.4CQRS、ES與流處理\h第二部分設(shè)計(jì)\h第4章DDD的DSL是什么\h4.1為什么DDD需要DSL\h4.1.1為什么實(shí)現(xiàn)DDD那么難\h4.1.2搞定DDD的“錘子”在哪里\h4.2需要什么樣的DSL\h4.2.1在“信仰”上保持中立\h4.2.2DDD原生\h4.2.3在復(fù)雜和簡(jiǎn)單中平衡\h4.2.4通過(guò)DSL重塑軟件開發(fā)過(guò)程\h4.3DDDML——DDD的DSL\h4.3.1DDDML的詞匯表\h4.3.2DDDML的Schema\h4.4DDDML示例:Car\h4.4.1“對(duì)象”的名稱在哪里\h4.4.2使用兩種命名風(fēng)格:camelCase與PascalCase\h4.4.3為何引入關(guān)鍵字itemType\h第5章限界上下文\h5.1DDDML文檔的根結(jié)點(diǎn)下有什么\h5.2限界上下文的配置\h5.3名稱空間\h5.3.1再談PascalCase命名風(fēng)格\h5.3.2注意兩個(gè)字母的首字母縮寫詞\h5.4關(guān)于模塊\h第6章值對(duì)象\h6.1領(lǐng)域基礎(chǔ)類型\h6.1.1例子:從OFBiz借鑒過(guò)來(lái)的類型系統(tǒng)\h6.1.2例子:任務(wù)的觸發(fā)器\h6.2數(shù)據(jù)值對(duì)象\h6.3枚舉對(duì)象\h第7章聚合與實(shí)體\h7.1用同一個(gè)結(jié)點(diǎn)描述聚合及聚合根\h7.2實(shí)體之間只有一種基本關(guān)系\h7.3關(guān)于實(shí)體的ID\h7.4不變的實(shí)體\h7.5動(dòng)態(tài)對(duì)象\h7.6繼承與多態(tài)\h7.6.1使用關(guān)鍵字inheritedFrom\h7.6.2超對(duì)象\h7.7引用\h7.7.1定義實(shí)體的引用\h7.7.2屬性的類型與引用類型\h7.8基本屬性與派生屬性\h7.8.1類型為實(shí)體集合的派生屬性\h7.8.2類型為值對(duì)象的派生屬性\h7.9約束\h7.9.1在實(shí)體層面的約束\h7.9.2在屬性層面的約束\h7.10提供擴(kuò)展點(diǎn)\h第8章超越數(shù)據(jù)模型\h8.1實(shí)體的方法\h8.1.1聚合根的方法\h8.1.2非聚合根實(shí)體的方法\h8.1.3屬性的命令\h8.1.4命令I(lǐng)D與請(qǐng)求者ID\h8.2記錄業(yè)務(wù)邏輯\h8.2.1關(guān)于accountingQuantityTypes\h8.2.2關(guān)于derivationLogic\h8.2.3關(guān)于filter\h8.2.4使用關(guān)鍵字referenceFilter\h8.2.5業(yè)務(wù)邏輯代碼中的變量\h8.2.6說(shuō)說(shuō)區(qū)塊鏈\h8.3領(lǐng)域服務(wù)\h8.4在方法定義中使用關(guān)鍵字inheritedFrom\h8.5方法的安全性\h第9章模式\h9.1賬務(wù)模式\h9.2狀態(tài)機(jī)模式\h9.3樹結(jié)構(gòu)模式\h9.3.1簡(jiǎn)單的樹\h9.3.2使用關(guān)鍵字structureType\h9.3.3使用關(guān)鍵字structureTypeFilter\h第三部分實(shí)踐\h第10章處理限界上下文與值對(duì)象\h10.1項(xiàng)目文件\h10.2處理值對(duì)象\h10.2.1一個(gè)需要處理的數(shù)據(jù)值對(duì)象示例\h10.2.2使用Hibernate存儲(chǔ)數(shù)據(jù)值對(duì)象\h10.2.3處理值對(duì)象的集合\h10.2.4在URL中使用數(shù)據(jù)值對(duì)象\h10.2.5處理領(lǐng)域基礎(chǔ)類型\h第11章處理聚合與實(shí)體\h11.1生成聚合的代碼\h11.1.1接口\h11.1.2代碼中的命名問(wèn)題\h11.1.3接口的實(shí)現(xiàn)\h11.1.4事件存儲(chǔ)與持久化\h11.1.5使用Validation框架\h11.1.6保證靜態(tài)方法與模型同步更新\h11.1.7不使用事件溯源\h11.2Override聚合對(duì)象的方法\h11.3處理繼承\(zhòng)h11.3.1TPCH\h11.3.2TPCC\h11.3.3TPS\h11.4處理模式\h11.4.1處理賬務(wù)模式\h11.4.2處理狀態(tài)機(jī)模式\h第12章處理領(lǐng)域服務(wù)\h12.1處理數(shù)據(jù)的一致性\h12.1.1使用數(shù)據(jù)庫(kù)事務(wù)實(shí)現(xiàn)一致性\h12.1.2使用Saga實(shí)現(xiàn)最終一致性\h12.2發(fā)布與處理領(lǐng)域事件\h12.2.1編寫DDDML文檔\h12.2.2生成的事件發(fā)布代碼\h12.2.3編寫生產(chǎn)端聚合的業(yè)務(wù)邏輯\h12.2.4實(shí)現(xiàn)消費(fèi)端領(lǐng)域事件的處理\h12.3支持基于編制的Saga\h12.3.1編寫DDDML文檔\h12.3.2生成的Saga命令處理代碼\h12.3.3需要我們編寫的Saga代碼\h12.3.4需要我們實(shí)現(xiàn)的實(shí)體方法\h第13章RESTfulAPI\h13.1RESTfulAPI的最佳實(shí)踐\h13.1.1沒有必要絞盡腦汁地尋找名詞\h13.1.2盡可能使用HTTP作為封包\h13.1.3異常處理\h13.2聚合的RESTfulAPI\h13.2.1GET\h13.2.2PUT\h13.2.3PATCH\h13.2.4DELETE\h13.2.5POST\h13.2.6事件溯源API\h13.2.7樹的查詢接口\h13.3服務(wù)的RESTfulAPI\h13.4身份與訪問(wèn)管理\h13.4.1獲取OAuth2.0BearerToken\h13.4.2在資源服務(wù)器上處理授權(quán)\h13.5生成ClientSDK\h13.5.1創(chuàng)建聚合實(shí)例\h13.5.2更新聚合實(shí)例\h13.5.3使用Retrofit2\h第14章直達(dá)UI\h14.1兩條路線的斗爭(zhēng)\h14.1.1前端“知道”領(lǐng)域模型\h14.1.2前端“只知道”RESTfulAPI\h14.2生成AdminUI\h14.2.1使用referenceFilter\h14.2.2展示派生的實(shí)體集合屬性\h14.2.3使用屬性層面的約束\h14.2.4使用UI層元數(shù)據(jù)\h14.2.5構(gòu)建更實(shí)時(shí)的應(yīng)用\h第四部分建模漫談與DDD隨想\h第15章找回敏捷的軟件設(shè)計(jì)\h15.1重構(gòu)不是萬(wàn)能靈藥\h15.2數(shù)據(jù)建模示例:訂單的裝運(yùn)與支付\h15.2.1訂單與訂單行項(xiàng)\h15.2.2訂單與訂單裝運(yùn)組\h15.2.3訂單與裝運(yùn)單\h15.2.4訂單的項(xiàng)目發(fā)貨\h15.2.5訂單的支付\h15.3中臺(tái)是一個(gè)輪回\h15.4實(shí)例化需求與行為驅(qū)動(dòng)測(cè)試\h15.4.1什么是實(shí)例化需求\h15.4.2BDD工具\(yùn)h15.4.3BDD工具應(yīng)與DDD相得益彰\h15.4.4不要在驗(yàn)收測(cè)試中使用固件數(shù)據(jù)\h15.4.5制造“制造數(shù)據(jù)”的工具\(yùn)h15.5要領(lǐng)域模型驅(qū)動(dòng),不要UI驅(qū)動(dòng)\h15.6不要用“我”的視角設(shè)計(jì)核心模型\h15.6.1讓User消失\h15.6.2認(rèn)識(shí)一下Party\h15.7我們想要的敏捷設(shè)計(jì)\h第16章說(shuō)說(shuō)SaaS\h16.1何為SaaS\h16.2多租戶技術(shù)\h16.3構(gòu)建成功的SaaS有何難\h16.3.1多租戶系統(tǒng)的構(gòu)建成本\h16.3.2難以滿足的定制化需求\h16.3.3負(fù)重前行的傳統(tǒng)軟件公司\h16.4SaaS需要DDD\h第17章更好的“錘子”\h17.1我們制作的一個(gè)DDDMLGUI工具\(yùn)h17.1.1給領(lǐng)域建模提供起點(diǎn)\h17.1.2創(chuàng)建新的限界上下文\h17.1.3從OFBiz中“借鑒”數(shù)據(jù)模型\h17.1.4構(gòu)建項(xiàng)目并運(yùn)行應(yīng)用\h17.1.5使用HTTPPUT方法創(chuàng)建實(shí)體\h17.1.6給聚合增加方法\h17.1.7生成限界上下文的DemoAdminUI\h17.1.8讓不同層級(jí)的開發(fā)人員各盡其能\h17.2以統(tǒng)一語(yǔ)言建模\h附錄DDDML示例與縮寫表注:原文檔電子版(非掃描),需要的請(qǐng)下載本文檔后留言謝謝。第一部分概念■第1章DDD的關(guān)鍵概念■第2章其他DDD相關(guān)概念■第3章CQRS與EventSourcing第1章DDD的關(guān)鍵概念開發(fā)大型軟件最難的部分并不是實(shí)現(xiàn),而是要深刻理解它所服務(wù)的現(xiàn)實(shí)世界的領(lǐng)域。領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)(Domain-DrivenDesign,DDD)是一種處理高度復(fù)雜領(lǐng)域的愿景(Vision)和方法,它主張?jiān)谲浖?xiàng)目中把領(lǐng)域本身作為關(guān)注的焦點(diǎn),維護(hù)一個(gè)對(duì)領(lǐng)域有深度認(rèn)知的軟件模型。這個(gè)愿景和方法,經(jīng)由建模專家EricEvans于2004年出版的其最具影響力的著名圖書Domain-DrivenDesign:TacklingComplexityintheHeartofSoftware\h[1](簡(jiǎn)稱DDD)正式面世。部分開發(fā)人員是不是覺得這段話不太好理解?也許大家可以先思考一個(gè)問(wèn)題:為什么命名是程序員公認(rèn)的軟件開發(fā)中最艱巨的任務(wù)?Quora網(wǎng)站曾進(jìn)行“最挑戰(zhàn)程序員的任務(wù)”的投票,半數(shù)程序員認(rèn)為最難的事情是“Namingthings”。排在第二位的是“ExplainingwhatIdo(ordon'tdo)”,其得票數(shù)大約只是前者的一半。其實(shí)命名的困難只是表象,構(gòu)建領(lǐng)域模型的困難才是根本。比如說(shuō),“對(duì)象”的名字代表著它的職責(zé),你只有把一個(gè)對(duì)象應(yīng)該干什么想清楚了,才能給它起一個(gè)恰當(dāng)?shù)拿帧!皩?duì)象應(yīng)該干什么”,這是一個(gè)領(lǐng)域建模問(wèn)題。我們可以在網(wǎng)上免費(fèi)獲取該書的縮寫精簡(jiǎn)版Domain-DrivenDesignQuickly\h[2],在InfoQ中文站中,有這個(gè)英文精簡(jiǎn)版對(duì)應(yīng)的中文翻譯版本\h[3]。關(guān)于DDD的關(guān)鍵概念,還可以參考維基百科的相關(guān)詞條\h[4]。領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)(DDD),其實(shí)就是以領(lǐng)域模型驅(qū)動(dòng)軟件設(shè)計(jì)。要理解DDD,關(guān)鍵是理解什么是DDD所指的領(lǐng)域模型,但在此之前,還是應(yīng)該先認(rèn)識(shí)一下軟件開發(fā)的過(guò)程。然后,基于此認(rèn)識(shí)重溫一下DDD在戰(zhàn)術(shù)以及戰(zhàn)略層面的若干關(guān)鍵概念。本章的最后會(huì)簡(jiǎn)單探討一下軟件開發(fā)團(tuán)隊(duì)經(jīng)常接觸到的幾種模型范式,幫助理解DDD模型和它們的關(guān)系。\h[1]EricEvans.Domain-DrivenDesign:TacklingComplexityintheHeartofSoftware.AddisonWesley,2003.見/exec/obidos/ASIN/0321125215/domainlanguag-20。\h[2]FloydMarinescu&AbelAvram.Domain-DrivenDesignQuickly.C4Media,2007.見/minibooks/domain-driven-design-quickly。\h[3]領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)精簡(jiǎn)版(全新修訂),/article/domain-driven-design-quickly-new。\h[4]Domain-DrivenDesign,/wiki/Domain-driven_design。1.1自頂而下、逐步求精我第一次接觸計(jì)算機(jī)程序設(shè)計(jì)是在上大學(xué)的時(shí)候,當(dāng)時(shí)學(xué)習(xí)的第一門編程語(yǔ)言是Fortran。記得就是在這門課上,老師告訴我們,開發(fā)程序應(yīng)該“自頂而下、逐步求精”。也就是說(shuō),不要求一步就編寫出可執(zhí)行的程序,我們可以面向問(wèn)題的總體目標(biāo),抽象低層的細(xì)節(jié),先構(gòu)造程序的高層結(jié)構(gòu),然后再一層一層地向下分解和細(xì)化,最后一步編寫出來(lái)的程序才是可執(zhí)行程序。這實(shí)際上是一種分而治之的思想。打個(gè)比喻,寫程序應(yīng)該像建造大樓一樣,一開始要有設(shè)計(jì)任務(wù)書(甲方的需求),然后設(shè)計(jì)單位根據(jù)任務(wù)書做概念設(shè)計(jì)、方案設(shè)計(jì)、初步設(shè)計(jì),可能還要進(jìn)行所謂的擴(kuò)大初步設(shè)計(jì),之后是施工圖設(shè)計(jì),最后施工單位拿著施工圖紙才能造出真正的建筑。當(dāng)時(shí)我就有醍醐灌頂之感,“自頂而下、逐步求精”,何其妙哉!后來(lái)我才知道,這句話其實(shí)來(lái)自瑞士計(jì)算機(jī)科學(xué)家尼古拉斯·沃斯在1971年發(fā)表的論文《通過(guò)逐步求精方式開發(fā)程序》(ProgramDevelopmentbyStepwiseRefinement)。這篇論文首次提出了結(jié)構(gòu)化程序設(shè)計(jì)(StructureProgramming)的概念。1983年1月,ACM在紀(jì)念CommunicationsoftheACM創(chuàng)刊25周年時(shí),從其1/4個(gè)世紀(jì)發(fā)表的論文中評(píng)選出了其中具有“里程碑意義的研究論文”25篇(每年一篇),沃斯的這篇論文就是其中之一。1.1.1DDD開創(chuàng)全新分析流派雖然到了20世紀(jì)80年代,面向?qū)ο蟮某绦蛟O(shè)計(jì)方法已經(jīng)開始大行其道,但是自頂而下、逐步求精的程序開發(fā)方法并沒有過(guò)時(shí)。就像建筑材料再怎么變化,想要建造一個(gè)偉大的建筑,需求分析、概念設(shè)計(jì)、方案設(shè)計(jì)、施工圖設(shè)計(jì)這些工作恐怕難以避免。那么,DDD做的到底是哪個(gè)階段的事情呢?筆者認(rèn)為DDD的領(lǐng)域建模階段大致相當(dāng)于建筑設(shè)計(jì)的需求分析與概念設(shè)計(jì)階段。相信任何經(jīng)歷過(guò)大型軟件開發(fā)項(xiàng)目的人都會(huì)贊成,想要提升軟件開發(fā)效率和軟件質(zhì)量、避免返工浪費(fèi),關(guān)鍵是要有高質(zhì)量的分析。高質(zhì)量的分析是需要有方法論的。軟件的分析也是一個(gè)“領(lǐng)域”,那么基于構(gòu)建一個(gè)“關(guān)于分析的模型”的方法,就產(chǎn)生了不同的流派??梢哉f(shuō),EricEvans的DDD開創(chuàng)了一個(gè)全新的分析流派。那么,這個(gè)流派有什么特點(diǎn)呢?首先,DDD是一種面向?qū)ο蠓治觯∣bject-OrientedAnalysis,OOA)與設(shè)計(jì)的方法論,可以很好地與現(xiàn)代的面向?qū)ο蟮某绦蛟O(shè)計(jì)(Object-OrientedProgramming,OOP)方法相結(jié)合,實(shí)現(xiàn)軟件的編程方法會(huì)反過(guò)來(lái)影響分析方法。DDD所使用的術(shù)語(yǔ),如對(duì)象(DDD將對(duì)象分為實(shí)體/引用對(duì)象、值對(duì)象)、屬性等,了解OOP的開發(fā)人員會(huì)感覺很熟悉。其次,DDD拋棄了分裂業(yè)務(wù)分析與軟件設(shè)計(jì)的做法,使用單一的領(lǐng)域模型來(lái)同時(shí)滿足這兩方面的要求??梢哉f(shuō),DDD致力于構(gòu)造一個(gè)易于編程實(shí)現(xiàn)(特別是使用OO語(yǔ)言)的概念模型。這里說(shuō)DDD是OOA的一種,可能會(huì)引起爭(zhēng)議。有人也許會(huì)堅(jiān)持認(rèn)為DDD只是面向?qū)ο笤O(shè)計(jì)(Object-OrientedDesign,OOD)。但是,既然DDD是反對(duì)分裂業(yè)務(wù)分析與軟件設(shè)計(jì)的,那么,我們就可以說(shuō)DDD既是OOA也是OOD。我甚至認(rèn)為,DDD從誕生之日起就是最有分量的OOA方法論。1.1.2什么是軟件的核心復(fù)雜性我們知道,Domain-DrivenDesign:TacklingComplexityintheHeartofSoftware一書的副標(biāo)題翻譯過(guò)來(lái)是“軟件核心復(fù)雜性應(yīng)對(duì)之道”,那么,這個(gè)軟件的核心復(fù)雜性(Complexity)是怎么來(lái)的呢?Complexity有繁雜之意。它的解釋之一是:因?yàn)榇嬖诤芏嘞嗷リP(guān)聯(lián)的部分而導(dǎo)致的狀況。對(duì)于大多數(shù)軟件系統(tǒng),它們的核心復(fù)雜性是由其服務(wù)的領(lǐng)域涉及的范圍帶來(lái)的。比如說(shuō),隨著領(lǐng)域內(nèi)的名詞概念增多,概念以及概念之間發(fā)生關(guān)系的可能性也會(huì)呈幾何級(jí)數(shù)增長(zhǎng),于是我們想要全面地理解領(lǐng)域就會(huì)變得越來(lái)越困難。所以,解決核心復(fù)雜性的關(guān)鍵還是在于切分(分而治之),也就是說(shuō)希望可以縮減每次要解決的領(lǐng)域問(wèn)題的范圍,簡(jiǎn)化概念和概念之間的關(guān)系。DDD正是這么做的。那么,DDD要構(gòu)建的領(lǐng)域模型到底是什么東西?它有哪些關(guān)鍵概念可以幫助我們實(shí)現(xiàn)“分而治之”呢?1.2什么是領(lǐng)域模型本書在提到領(lǐng)域模型的時(shí)候,一般特指DDD的領(lǐng)域模型,也就是EricEvans在Domain-DrivenDesign:TacklingComplexityintheHeartofSoftware一書中闡述的那種領(lǐng)域模型。DDD要求領(lǐng)域模型既能反映對(duì)領(lǐng)域的深度認(rèn)知,又能直接用于指導(dǎo)軟件的設(shè)計(jì)與實(shí)現(xiàn)。我們可以說(shuō)領(lǐng)域模型是系統(tǒng)化的領(lǐng)域知識(shí)。不系統(tǒng)化的知識(shí)是難以傳授、掌握和應(yīng)用的。想象一下,一個(gè)會(huì)計(jì)專業(yè)的學(xué)生如果不去學(xué)習(xí)《會(huì)計(jì)基礎(chǔ)》課本,直接上崗記賬會(huì)出現(xiàn)什么樣的狀況,恐怕會(huì)記得一塌糊涂吧?為了將領(lǐng)域知識(shí)系統(tǒng)化,我們需要做領(lǐng)域分析,而分析得到的結(jié)果是一個(gè)體現(xiàn)我們對(duì)領(lǐng)域認(rèn)知的概念模型。那么,我們做領(lǐng)域分析的時(shí)候,是不是可以只專注于做好業(yè)務(wù)領(lǐng)域的分析,構(gòu)建一個(gè)只是反映業(yè)務(wù)知識(shí)的分析模型呢?《領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)精簡(jiǎn)版》第13頁(yè)中對(duì)這個(gè)問(wèn)題有論述:一個(gè)推薦的設(shè)計(jì)技術(shù)是創(chuàng)建分析模型,它被認(rèn)為是與代碼設(shè)計(jì)相互分離、通常是由不同的人完成的。分析模型是業(yè)務(wù)領(lǐng)域分析的結(jié)果,此模型不需要考慮軟件如何實(shí)現(xiàn)。這樣的一個(gè)模型可用來(lái)理解領(lǐng)域,它建立了特定級(jí)別的知識(shí),模型在分析層面是正確的。軟件實(shí)現(xiàn)不是這個(gè)階段要考慮的,因?yàn)檫@會(huì)被看作一個(gè)導(dǎo)致混亂的因素。這個(gè)模型到達(dá)開發(fā)人員那里后,由他們來(lái)做設(shè)計(jì)的工作。因?yàn)檫@個(gè)模型中沒有包含設(shè)計(jì)原則,它可能無(wú)法很好地為目標(biāo)服務(wù)。因此開發(fā)人員不得不修改它,或者創(chuàng)建分離的設(shè)計(jì),在模型和代碼之間也不再存在映射關(guān)系,最終的結(jié)果是分析模型在編碼開始后就被拋棄了。為什么“分析模型在編碼開始后就被拋棄”是一個(gè)大問(wèn)題?為什么領(lǐng)域模型與代碼之間應(yīng)該存在映射關(guān)系?是時(shí)候重溫這句經(jīng)典名言了:程序是寫來(lái)給人讀的,只會(huì)偶爾讓機(jī)器執(zhí)行一下?!狝belson和Sussman與建筑不同,軟件具有易變性,在軟件開發(fā)出來(lái)之后,可能還需要經(jīng)常修改。只有反映高層結(jié)構(gòu)的代碼才是易于閱讀和理解的。我們都知道,開發(fā)人員閱讀代碼的時(shí)間遠(yuǎn)遠(yuǎn)多于編寫代碼的時(shí)間。Google的工程師每人每天大約只產(chǎn)出100行新代碼!所以,Evans給出了他認(rèn)為的更好的分析建?!?jiǎng)?chuàng)建軟件高層結(jié)構(gòu)——的建議:一種更好的方法是將領(lǐng)域建模和設(shè)計(jì)緊密關(guān)聯(lián)起來(lái)。模型在構(gòu)建時(shí)就考慮到軟件的實(shí)現(xiàn)和設(shè)計(jì)。開發(fā)人員應(yīng)該被加入到建模的過(guò)程中。主要的想法是選擇一個(gè)能夠在軟件實(shí)現(xiàn)中恰當(dāng)?shù)乇磉_(dá)的模型,這樣設(shè)計(jì)過(guò)程會(huì)很順暢并且是基于模型的。將代碼與其所基于的模型緊密關(guān)聯(lián),將會(huì)使得代碼更有意義,并且與模型保持相關(guān)?!额I(lǐng)域驅(qū)動(dòng)設(shè)計(jì)精簡(jiǎn)版》,第14頁(yè)那么,DDD的領(lǐng)域模型到底是什么?還是聽聽EricEvans的“官方”說(shuō)法吧:領(lǐng)域模型不是一幅具體的圖,它是那幅圖想要去傳達(dá)的那個(gè)思想。它也不是一個(gè)領(lǐng)域?qū)<翌^腦中的知識(shí),而是一個(gè)經(jīng)過(guò)嚴(yán)格組織并進(jìn)行選擇性抽象的知識(shí)。一幅圖能夠描繪和傳達(dá)一個(gè)模型,同樣,經(jīng)過(guò)精心編寫的代碼和一段英語(yǔ)句子都能達(dá)到這個(gè)目的?!额I(lǐng)域驅(qū)動(dòng)設(shè)計(jì)精簡(jiǎn)版》,第3頁(yè)1.3戰(zhàn)術(shù)層面的關(guān)鍵概念由于本書并不是DDD的入門書,所以未對(duì)DDD在戰(zhàn)術(shù)層面的一些關(guān)鍵概念進(jìn)行詳細(xì)解釋,讀者可以查閱DDD相關(guān)圖書或網(wǎng)上文章進(jìn)行了解。比如《領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)精簡(jiǎn)版》在第3章中詳細(xì)介紹了模型驅(qū)動(dòng)設(shè)計(jì)的基本構(gòu)成要素,也就是DDD戰(zhàn)術(shù)層面的關(guān)鍵概念。本書只結(jié)合筆者自身的理解,強(qiáng)調(diào)一些筆者認(rèn)為最重要的概念。1.3.1實(shí)體有一類對(duì)象擁有標(biāo)識(shí)符(簡(jiǎn)稱ID),不管對(duì)象的狀態(tài)如何變化,它的ID總是不變的,這樣的對(duì)象稱為實(shí)體。比如說(shuō),我們的銀行賬戶(Account)總是有一個(gè)編號(hào)(賬號(hào))。我們存錢、取錢時(shí)賬戶里面的錢會(huì)發(fā)生變化,但是賬號(hào)不變。我們通過(guò)這個(gè)賬號(hào),能夠查詢賬戶的余額。所以,我們可以把銀行賬戶建模為一個(gè)實(shí)體,選擇它的編號(hào)作為這個(gè)實(shí)體的ID。對(duì)于很多開發(fā)人員來(lái)說(shuō),實(shí)體是他們非常熟悉的概念。特別是對(duì)于使用過(guò)ORM\h[1]框架的開發(fā)人員來(lái)說(shuō),當(dāng)你提到“實(shí)體”,他們可能馬上就會(huì)想到“就是需要映射為數(shù)據(jù)表(Table)的那些對(duì)象”。\h[1]Object-relationalMapping(ORM),/wiki/Object-relational_mapping。1.3.2值對(duì)象下面這幾句話是從Domain-DrivenDesign:TacklingComplexityintheHeartofSoftware一書中摘錄的(有少量改寫),是筆者認(rèn)為理解DDD值對(duì)象的關(guān)鍵點(diǎn):·用來(lái)描述領(lǐng)域的特定方面的、沒有標(biāo)識(shí)符的對(duì)象,叫作值對(duì)象?!ず雎云渌愋偷膶?duì)象(如Service、Repository、Factory等),假設(shè)對(duì)象只有實(shí)體和值對(duì)象兩種,若將那些符合實(shí)體定義的對(duì)象作為實(shí)體,那么剩下的對(duì)象就是值對(duì)象?!ね扑]將值對(duì)象實(shí)現(xiàn)為“不可變的”(Immutable)。也就是說(shuō),值對(duì)象由一個(gè)構(gòu)造器創(chuàng)建,并且在它們的生命周期內(nèi)永遠(yuǎn)不會(huì)被修改。實(shí)現(xiàn)為不可變的,并且不具有標(biāo)識(shí)符后,值對(duì)象就能夠被安全地共享,并且能維持一致性。對(duì)于程序員來(lái)說(shuō),還可以這么理解:如果你熟悉的語(yǔ)言中存在所謂的基本類型(PrimitiveType),那么一般來(lái)說(shuō),它們都可以被理解為值對(duì)象。比如Java中的byte、int、long、float、boolean等,都是值對(duì)象。Java基礎(chǔ)類庫(kù)的很多類都是值對(duì)象。比如對(duì)金額進(jìn)行計(jì)算時(shí),在Java代碼中一般會(huì)使用BigDecimal來(lái)實(shí)現(xiàn),而BigDecimal就是一個(gè)表示大十進(jìn)制數(shù)的值對(duì)象。BigDecimal的實(shí)現(xiàn)使用了兩個(gè)關(guān)鍵的私有字段(privatefield),其中字段intVal的類型是BigInteger,表示“大整數(shù)”,另外一個(gè)字段scale的類型是int(整數(shù)),用來(lái)表示小數(shù)的位數(shù)。比如,一個(gè)BigDecimal對(duì)象字段intVal的值為10011,字段scale的值為2,那么這個(gè)BigDecimal對(duì)象表示的就是“100.11”。BigInteger的實(shí)現(xiàn)主要依賴一個(gè)int[](整數(shù)的數(shù)組)類型的字段,通過(guò)使用這個(gè)數(shù)組可以表示任意大的整數(shù)。BigInteger也是一個(gè)值對(duì)象。如果我們使用Java語(yǔ)言實(shí)現(xiàn)一個(gè)表示“錢”的Money值對(duì)象,內(nèi)部可以用一個(gè)私有的BigDecimal類型的字段來(lái)表示金額,然后用一個(gè)String類型的字段來(lái)表示貨幣單位的縮寫。它的構(gòu)造器可能像這樣:publicMoney(BigDecimalamount,Stringcurrency);當(dāng)我們需要“人民幣10元錢”的時(shí)候,可以按如下方式實(shí)例化一個(gè)對(duì)象:MoneytenCny=newMoney(BigDecimal.valueOf(10),"CNY");一旦這筆錢構(gòu)造出來(lái),就是一個(gè)不可變的整體。Money的加、減、乘、除,以及(按匯率)轉(zhuǎn)換為以另一種貨幣單位表示的“錢”,都需要調(diào)用相應(yīng)的方法來(lái)完成,這些方法返回的結(jié)果會(huì)是一個(gè)新的不可變的Money對(duì)象。在Java開源社區(qū)有一個(gè)JodaMoney\h[1]類庫(kù),其中Money類的實(shí)現(xiàn)就類似上面描述的形式。我們應(yīng)該盡可能地多用不可變的對(duì)象,而不是隨意地為每個(gè)字段創(chuàng)建相應(yīng)的getter/setter方法(也就是可讀可寫的屬性)。其實(shí)在很多領(lǐng)域中都需要這樣的封裝。舉例來(lái)說(shuō),我們可能希望在開發(fā)應(yīng)用時(shí)按如下方式使用值對(duì)象:·可以聲明一個(gè)屬性的類型是Email,而不是String。·可以聲明一個(gè)屬性的類型是手機(jī)號(hào)(MobileNumber),而不是String?!た梢月暶饕粋€(gè)屬性的類型是郵政編碼(PostalCode),而不是長(zhǎng)整數(shù)(Long)。使用過(guò)HibernateORM的開發(fā)人員可能會(huì)注意到在Hibernate中有一個(gè)概念是DependentObjects(非獨(dú)立的對(duì)象),可以認(rèn)為它所指的就是值對(duì)象。DependentObjects是不會(huì)被映射為數(shù)據(jù)庫(kù)中的表的,它們會(huì)被映射為表中的列。\h[1]見/joda-money/。1.3.3聚合與聚合根、聚合內(nèi)部實(shí)體在筆者看來(lái),聚合(Aggregate)是DDD在戰(zhàn)術(shù)層面最為重要的一個(gè)概念。它是DDD可以在戰(zhàn)術(shù)層面應(yīng)對(duì)軟件核心復(fù)雜性的關(guān)鍵。什么是聚合?聚合在對(duì)象之間,特別是實(shí)體與實(shí)體之間劃出邊界。聚合內(nèi)的實(shí)體分為兩種:聚合根(AggregateRoot)與聚合內(nèi)部實(shí)體(或者非聚合根實(shí)體)。在本書的行文中可能會(huì)使用以下幾種說(shuō)法:·聚合內(nèi)實(shí)體,指聚合內(nèi)包括聚合根的那些實(shí)體,一般不會(huì)把聚合根排除在外。具體含義讀者可以根據(jù)上下文判斷?!ぞ酆蟽?nèi)部實(shí)體,把聚合根排除在外的聚合內(nèi)部的實(shí)體。·非聚合根實(shí)體,非常明確地把聚合根排除在外的聚合內(nèi)部的實(shí)體。需要避免產(chǎn)生歧義時(shí)會(huì)使用這個(gè)說(shuō)法。一個(gè)聚合只能包含一個(gè)聚合根。當(dāng)客戶端需要訪問(wèn)一個(gè)聚合內(nèi)部實(shí)體的狀態(tài)時(shí),最先能得到的只有聚合根,然后通過(guò)這個(gè)聚合根,才能進(jìn)一步訪問(wèn)到聚合內(nèi)的其他實(shí)體。從一個(gè)聚合根出發(fā)能夠訪問(wèn)到的實(shí)體可以認(rèn)為是一個(gè)整體。聚合內(nèi)部實(shí)體的生命周期由它們所屬的聚合根控制。如果聚合根不存在,那么在它控制下的聚合內(nèi)部實(shí)體也就不存在了。很多時(shí)候,一個(gè)聚合內(nèi)只有聚合根這一個(gè)實(shí)體。有人聲稱,從經(jīng)驗(yàn)上判斷,大約有70%的聚合只有一個(gè)實(shí)體。實(shí)體又被稱為引用對(duì)象,這個(gè)名稱與值對(duì)象的概念相對(duì)。本書中提到聚合根、實(shí)體、值對(duì)象的時(shí)候,多數(shù)情況下并不會(huì)特別說(shuō)明是指這些對(duì)象的“類型”還是指它們的“實(shí)例”,請(qǐng)讀者自行根據(jù)上下文判斷。我們應(yīng)該把一個(gè)聚合根的實(shí)例以及生命周期完全受它控制的那些聚合內(nèi)部實(shí)體的實(shí)例作為一個(gè)整體來(lái)看待,對(duì)于這樣一個(gè)整體,有時(shí)候書中會(huì)使用“聚合實(shí)例”這個(gè)說(shuō)法。為了更好地理解什么是聚合、聚合根、聚合內(nèi)部實(shí)體,下面舉例說(shuō)明。這個(gè)例子包含訂單(Order)、訂單頭(OrderHeader)、訂單行項(xiàng)(OrderItem)三個(gè)互相關(guān)聯(lián)的概念。想象我們?cè)诮o一個(gè)電商系統(tǒng)構(gòu)建訂單相關(guān)的模型,我們可能會(huì)得到:·一個(gè)叫作Order的聚合?!み@個(gè)訂單聚合的聚合根是一個(gè)叫作OrderHeader的實(shí)體。實(shí)體OrderHeader的ID叫作OrderId(訂單號(hào))?!ねㄟ^(guò)OrderHeader實(shí)體,我們可以訪問(wèn)OrderItem實(shí)體的一個(gè)集合。OrderItem這個(gè)實(shí)體的局部ID叫作ProductId(產(chǎn)品ID)。因?yàn)闃I(yè)務(wù)邏輯可能已經(jīng)明確不允許在同一個(gè)訂單的不同訂單行項(xiàng)內(nèi)出現(xiàn)同一個(gè)產(chǎn)品,所以我們可以選擇產(chǎn)品ID作為訂單行項(xiàng)的局部ID(LocalID)。顯然,在這里,Order聚合與OrderHeader聚合根的名字并不相同,這體現(xiàn)了聚合與聚合根這兩個(gè)概念的微妙區(qū)別。但是確實(shí)在大多數(shù)時(shí)候,聚合和它的聚合根擁有同樣的名字,甚至在大多數(shù)時(shí)候,一個(gè)聚合內(nèi)就只有聚合根這一個(gè)實(shí)體。1.3.4聚合的整體與局部聚合內(nèi)的實(shí)體,從聚合根到其他實(shí)體,往往是一個(gè)強(qiáng)烈的整體與局部的關(guān)系。這意味著對(duì)聚合內(nèi)部的非聚合根實(shí)體而言,它的ID(全局ID)極有可能最少由兩個(gè)部分組成:一部分是聚合根的ID,另一部分是它在聚合內(nèi)的局部ID。比如說(shuō),OrderItem的局部ID可能是ProductId。如果允許同一個(gè)訂單的不同行項(xiàng)出現(xiàn)相同的產(chǎn)品,那么最少你還可以創(chuàng)造一個(gè)叫作“行號(hào)(SequenceId.)”的概念作為訂單行項(xiàng)的局部ID。再比如,人(作為聚合根)有左手和右手,那么,“左”或“右”就是“手”(作為聚合內(nèi)部實(shí)體)的局部ID。關(guān)于全局ID與局部ID的問(wèn)題,后文還會(huì)進(jìn)一步探討。1.3.5聚合是數(shù)據(jù)修改的單元可以從這個(gè)角度考慮聚合的邊界:如果兩個(gè)實(shí)體之間的狀態(tài)出現(xiàn)不一致(即使只有非常短暫的一瞬間我們能看到它們的不一致)就讓人難以接受的話,那么這兩個(gè)實(shí)體屬于同一個(gè)聚合;否則它們就不屬于一個(gè)聚合。比如,也許我們不能接受一個(gè)訂單的所有訂單行項(xiàng)的金額之和不等于訂單頭的總金額。那么,我們可以把訂單頭與訂單行項(xiàng)這兩個(gè)實(shí)體劃歸到一個(gè)聚合內(nèi)。對(duì)于一個(gè)聚合內(nèi)的對(duì)象狀態(tài)(數(shù)據(jù))的修改,我們需要保證它們總是一致的,也就是說(shuō)我們要實(shí)現(xiàn)它們的強(qiáng)一致性。聚合是數(shù)據(jù)修改的單元,這個(gè)觀點(diǎn)其實(shí)已經(jīng)被廣泛接受,所以有人說(shuō)大多數(shù)NoSQL數(shù)據(jù)庫(kù)都是聚合型數(shù)據(jù)庫(kù)。因?yàn)橹髁鞯腘oSQL數(shù)據(jù)庫(kù),不管是文檔型的MongoDB,還是KV型的Redis,它們最少都能保證一個(gè)聚合內(nèi)的數(shù)據(jù)的強(qiáng)一致性,不管它們把這個(gè)聚合叫作文檔還是叫作Value。你可能聽說(shuō)過(guò)所謂“聚合內(nèi)強(qiáng)一致,聚合外最終一致”的建議。它的意思是,對(duì)于同一個(gè)聚合內(nèi)實(shí)體的數(shù)據(jù)(狀態(tài))的一致性,開發(fā)人員可以使用數(shù)據(jù)庫(kù)系統(tǒng)提供的ACID事務(wù)來(lái)實(shí)現(xiàn)強(qiáng)一致性;對(duì)于不同聚合之間的數(shù)據(jù),開發(fā)人員應(yīng)該優(yōu)先考慮自己編碼來(lái)實(shí)現(xiàn)最終一致性。DDD社區(qū)中很多人都贊同這個(gè)實(shí)踐。強(qiáng)一致性模型與最終一致性模型是兩種不同的數(shù)據(jù)一致性模型,這里的模型可以理解為數(shù)據(jù)庫(kù)系統(tǒng)與開發(fā)者之間的契約(Contract)。強(qiáng)一致性模型是指,開發(fā)者只要按照數(shù)據(jù)庫(kù)系統(tǒng)的某些規(guī)則來(lái)做,就可以保證數(shù)據(jù)絕對(duì)不會(huì)不一致。最終一致性模型是指,開發(fā)者按照一定的規(guī)則來(lái)做,數(shù)據(jù)庫(kù)中的數(shù)據(jù)項(xiàng)在處理“過(guò)程中”可能會(huì)出現(xiàn)不一致,但是在處理過(guò)程結(jié)束后,它們最終可以達(dá)到一致。1.3.6聚合分析是“拆分”的基礎(chǔ)分布式系統(tǒng)設(shè)計(jì)原則的第一條:不要“分布”。確切地說(shuō),如果沒有足夠的理由就不要做分布式設(shè)計(jì),開發(fā)和運(yùn)維分布式系統(tǒng)往往需要更高的成本。為什么要分布?常見的理由是為了解決系統(tǒng)的水平擴(kuò)展問(wèn)題。這往往意味著我們需要給系統(tǒng)中的軟件元素(構(gòu)造塊)劃分出邊界,這樣我們就可以更有針對(duì)性地對(duì)每個(gè)構(gòu)造塊進(jìn)行獨(dú)立的優(yōu)化。此外,我們可能還需要對(duì)同一類型的數(shù)據(jù)(比如說(shuō)訂單數(shù)據(jù))進(jìn)行橫向切分(也就是所謂的Sharding),讓每個(gè)分布式系統(tǒng)的結(jié)點(diǎn)只負(fù)責(zé)處理其中的一部分?jǐn)?shù)據(jù)。那么,到底什么東西能分、什么東西不能分,它們之間的邊界如何表述呢?這就是聚合這個(gè)概念發(fā)揮作用的地方。有些東西互相關(guān)聯(lián)、密不可分,那么它們可能應(yīng)該建模成一個(gè)“聚合”。如果從一開始,在軟件設(shè)計(jì)中就使用了“聚合”這個(gè)概念的話,實(shí)際上表明我們已經(jīng)縱向地給其中的軟件元素劃分了邊界,也給數(shù)據(jù)的橫向切分(Sharding)提供了標(biāo)準(zhǔn)。比如說(shuō),如果我們通過(guò)聚合分析已經(jīng)把OrderHeader和OrderItem劃分到一個(gè)聚合內(nèi),將它們與其他聚合(比如Product、Payment等)區(qū)分開來(lái),我們可能會(huì)考慮開發(fā)一個(gè)叫作OrderService的可獨(dú)立部署的微服務(wù)。如果要做訂單——OrderHeader以及它關(guān)聯(lián)的OrderItem——數(shù)據(jù)的橫向切分,我們只要根據(jù)聚合根的ID(可能我們把這個(gè)ID叫作OrderId)來(lái)進(jìn)行SQL數(shù)據(jù)庫(kù)的分庫(kù)分表操作就行了。必要的時(shí)候,我們可以直接把某些聚合的數(shù)據(jù)存儲(chǔ)到NoSQL數(shù)據(jù)庫(kù)中。遺憾的是,有一些DDD的框架和工具,僅僅滿足于使用“一個(gè)聚合只有一個(gè)實(shí)體”的例子來(lái)展示它們的“強(qiáng)大”。我們認(rèn)為,一個(gè)DDD工具如果不支持聚合概念,那它就是一個(gè)“有殘疾”的DDD工具。雖然據(jù)說(shuō)按照經(jīng)驗(yàn)估計(jì),使用DDD方法建模,70%的聚合都只有一個(gè)實(shí)體(即只有聚合根這個(gè)實(shí)體),但是這并不等于聚合分析不重要。領(lǐng)域中真正復(fù)雜的業(yè)務(wù)邏輯,可能大部分都集中在另外30%的聚合以及需要處理不同聚合之間的數(shù)據(jù)一致性的領(lǐng)域服務(wù)內(nèi),而聚合分析尤其能為數(shù)據(jù)一致性的處理提供幫助。如果一個(gè)DDD工具不能真正地支持聚合概念,那么使用它的產(chǎn)品人員、系統(tǒng)分析師、開發(fā)人員在分析階段很可能也就不會(huì)認(rèn)真去做聚合分析,因?yàn)椤凹幢惴治隽?,大家也未必真的按照這個(gè)來(lái)做”。如果一個(gè)DDD框架不允許聚合內(nèi)存在多個(gè)“實(shí)體”,那么實(shí)際上就沒有了聚合這個(gè)概念(當(dāng)然,若只剩下大部分開發(fā)人員都耳熟能詳?shù)摹皩?shí)體”這一概念,確實(shí)很“方便”大家理解),如此一來(lái)我們采用強(qiáng)一致性模型的邊界在哪里?如果沒有明確的標(biāo)準(zhǔn),“哪幾個(gè)實(shí)體的數(shù)據(jù)需要保證強(qiáng)一致”這樣的決定應(yīng)該如何做出?可能有人覺得可以做二選一的設(shè)計(jì)決策:·要么是當(dāng)要改變狀態(tài)時(shí),都先開始一個(gè)數(shù)據(jù)庫(kù)事務(wù),操作完若干實(shí)體/表后提交數(shù)據(jù)庫(kù)事務(wù),讓數(shù)據(jù)庫(kù)事務(wù)來(lái)保證強(qiáng)一致性。這其實(shí)是我們常見的傳統(tǒng)做法?!ひ词钦J(rèn)為每個(gè)實(shí)體就構(gòu)成了一個(gè)聚合,當(dāng)要改變系統(tǒng)狀態(tài)的時(shí)候,都自己寫代碼來(lái)實(shí)現(xiàn)“最終一致性”。如果系統(tǒng)的最終用戶(業(yè)務(wù)部門)能接受所有實(shí)體的狀態(tài)只要最終一致就可以,那么這么做至少理論上是可行的。但是現(xiàn)實(shí)中這兩種(極端)做法在開發(fā)大型應(yīng)用,特別是互聯(lián)網(wǎng)應(yīng)用時(shí),往往都不太現(xiàn)實(shí)。前者除了很容易導(dǎo)致代碼層面的緊密耦合以外,往往還會(huì)造成嚴(yán)重的性能和擴(kuò)展性問(wèn)題,為了解決這些問(wèn)題可能需要大規(guī)模地重構(gòu)代碼甚至要重寫整個(gè)系統(tǒng)??赡苡腥藭?huì)想:那就不要那么極端,我們可以不只是“聚合內(nèi)強(qiáng)一致”,也“適當(dāng)”地多用數(shù)據(jù)庫(kù)事務(wù)來(lái)做“聚合外強(qiáng)一致性”。特別是在軟件開發(fā)的初始階段,軟件往往還是個(gè)單體應(yīng)用,數(shù)據(jù)庫(kù)可能還是一個(gè)單點(diǎn),一時(shí)半會(huì)兒也不會(huì)分庫(kù)分表,可以讓開發(fā)人員“便宜行事”,自行控制數(shù)據(jù)庫(kù)事務(wù)來(lái)保證在必要范圍內(nèi)的“強(qiáng)一致性”。現(xiàn)實(shí)情況是,“便宜行事”的開發(fā)人員很有可能“做過(guò)頭”,因?yàn)楦緵]有清晰的“邊界”。極端的情況就是臭名昭著的“OpenSessionInView”,也就是服務(wù)端收到客戶端請(qǐng)求時(shí)立即開啟一個(gè)數(shù)據(jù)庫(kù)事務(wù),在將請(qǐng)求的處理結(jié)果返回客戶端前的那一刻才提交數(shù)據(jù)庫(kù)事務(wù),這樣所有對(duì)系統(tǒng)狀態(tài)的查詢和修改操作都會(huì)放到同一個(gè)數(shù)據(jù)庫(kù)事務(wù)中完成——可以說(shuō)是非常“方便”了。但是,這樣做的結(jié)果往往是災(zāi)難級(jí)的系統(tǒng)性能表現(xiàn),以及剪不斷理還亂的面條式代碼。后一種做法,可能有人覺得不是問(wèn)題。他們可能會(huì)說(shuō):“我們就是應(yīng)該堅(jiān)持原則,一個(gè)實(shí)體一個(gè)聚合,所有實(shí)體之間都最終一致。業(yè)務(wù)部門要的是結(jié)果,他們才不管什么強(qiáng)一致還是最終一致,銀行轉(zhuǎn)賬這么重要的事情,大家還不是普遍接受‘最終一致’?所以,經(jīng)過(guò)精心設(shè)計(jì)的業(yè)務(wù)邏輯都是可以繞過(guò)大多數(shù)對(duì)強(qiáng)一致性的‘偽需求’的?!边@里所謂接受“最終一致”,意味著要接受“有時(shí)候數(shù)據(jù)就是不一致的”。比如,我們大多數(shù)時(shí)候都能接受出現(xiàn)這樣的情況:我們執(zhí)行了一個(gè)轉(zhuǎn)賬操作,源賬戶減少了500塊錢,目標(biāo)賬戶沒有馬上增加500塊錢。首先,我們認(rèn)為實(shí)體之間的強(qiáng)一致性需求一定是存在的,我們不能寄希望于“最終一致性”能滿足所有的業(yè)務(wù)需要。就算退一萬(wàn)步,業(yè)務(wù)部門確認(rèn)“最終一致性”真的能滿足他們的所有需求,這種做法也會(huì)極大地增加軟件開發(fā)的工作量與成本。如果大量處理“最終一致性”的代碼都需要程序員來(lái)“手動(dòng)”編碼實(shí)現(xiàn)的話,即使有一些框架和工具能提供一定的幫助,程序員也會(huì)不堪重負(fù),產(chǎn)出的可能是Bug滿滿的低質(zhì)量代碼。所以軟件開發(fā)團(tuán)隊(duì)幾乎總是會(huì)混合使用強(qiáng)一致性和最終一致性模型。如果開發(fā)團(tuán)隊(duì)想就這個(gè)問(wèn)題——什么時(shí)候選擇強(qiáng)一致性模型、什么時(shí)候選擇最終一致性模型——確定標(biāo)準(zhǔn),那么即使不使用“聚合”這個(gè)術(shù)語(yǔ),遲早也會(huì)發(fā)明一個(gè)相似的概念??傊?,聚合分析的意義就是讓開發(fā)人員一開始就在強(qiáng)一致性和最終一致性的選擇上進(jìn)行足夠的思考和權(quán)衡,而不是沒有想清楚就匆忙進(jìn)入編碼階段。如果先做了聚合分析,即使因?yàn)轫?xiàng)目工期的要求,沒有完全按照“聚合內(nèi)強(qiáng)一致,聚合外最終一致”的原則來(lái)編寫代碼,至少開發(fā)人員也清楚地知道自己在哪些實(shí)現(xiàn)代碼上是做了妥協(xié)的。也許有人會(huì)認(rèn)為既然要妥協(xié),那就沒有必要做預(yù)先的分析和設(shè)計(jì)。但是筆者認(rèn)為,就現(xiàn)狀而言,對(duì)軟件進(jìn)行預(yù)先設(shè)計(jì)的價(jià)值已經(jīng)被太多人低估了。順便說(shuō)一下,做聚合分析還有其他好處。一個(gè)好處是可以根據(jù)聚合分析的結(jié)果自動(dòng)生成UI層的代碼——最少是可以生成腳手架代碼。想一想訂單頭和訂單行項(xiàng)同屬一個(gè)訂單聚合的情況,它們?cè)谂c分屬不同聚合的兩個(gè)實(shí)體各自獨(dú)立、毫不相干的情況比起來(lái),顯然應(yīng)該是不一樣的吧?這個(gè)問(wèn)題后面可以進(jìn)一步探討。此外,聚合分析有助于自動(dòng)生成CQRS的“讀模型”(后面會(huì)討論CQRS模式,這里的讀模型可以先理解為數(shù)據(jù)庫(kù)的視圖),因?yàn)槲覀冎谰酆蟽?nèi)的實(shí)體關(guān)系密切,需要一起查詢的可能性很高。比如,因?yàn)橛唵尉酆系拇嬖?,我們可以自?dòng)生成聯(lián)接(Join)了訂單頭和訂單行項(xiàng)兩個(gè)表(實(shí)體)的視圖(讀模型)。1.3.7服務(wù)系統(tǒng)中有一些行為是不適合“歸屬于”哪個(gè)對(duì)象的,DDD建議把這樣的行為定義為服務(wù)(Service)?;蛘哒f(shuō),當(dāng)有一個(gè)操作需要修改多個(gè)聚合實(shí)例的狀態(tài)時(shí),這個(gè)操作就很有可能應(yīng)該被定義為服務(wù)。“服務(wù)”這個(gè)名詞在軟件開發(fā)過(guò)程中實(shí)在是被用“濫”了。當(dāng)需要與其他“服務(wù)”進(jìn)行區(qū)分時(shí),會(huì)稱這里所說(shuō)的“服務(wù)”為“領(lǐng)域服務(wù)”。舉個(gè)例子,我們開發(fā)一個(gè)賬務(wù)系統(tǒng)的時(shí)候,可能需要支持轉(zhuǎn)賬功能。所謂的轉(zhuǎn)賬,就是在一個(gè)賬目(Account)上扣減一定金額的錢,在另外一個(gè)賬目上增加相應(yīng)金額的錢。那么這個(gè)轉(zhuǎn)賬行為作為賬目的一個(gè)方法來(lái)定義是不是合適呢?如果是作為賬目的方法,那么它是應(yīng)該這樣定義(Java代碼):publicclassAccount{

publicvoidtransferFrom(AccountsourceAccount,Moneyamount){

//…

}

}還是應(yīng)該這樣定義:publicclassAccount{

publicvoidtransferTo(AccountdestinationAccount,Moneyamount){

//…

}

}我相信,上面兩種做法都會(huì)引起(不必要的)爭(zhēng)議。何不按如下形式定義一個(gè)轉(zhuǎn)賬服務(wù)呢(Java代碼):publicinterfaceTransferService{

voidtransfer(AccountsourceAccount,AccountdestinationAccount,Moneyamount);

}對(duì)于這個(gè)轉(zhuǎn)賬服務(wù)來(lái)說(shuō),它要改變的是兩個(gè)Account聚合實(shí)例的狀態(tài),雖然這兩個(gè)實(shí)例的類型都是Account,但是仍然屬于所謂的“跨聚合(實(shí)例)”的操作,在這種情況下,將其定義為服務(wù)比較合適。在DDD中,還有其他Repository(存儲(chǔ)庫(kù))、Factory(工廠)等戰(zhàn)術(shù)層面的概念,建議讀者自行閱讀DDD相關(guān)圖書或者到Google上查詢資料。1.4戰(zhàn)略層面的關(guān)鍵概念相信很多人都見過(guò)這樣的情形:從小項(xiàng)目演化而來(lái)的大系統(tǒng)最終變成開發(fā)團(tuán)隊(duì)的噩夢(mèng)。這些噩夢(mèng)幾乎無(wú)一例外地源于軟件的概念完整性(ConceptualIntegrity)遭到了破壞(很少是因?yàn)閱渭兊募夹g(shù)原因)。代碼可能是一代又一代的開發(fā)人員各行其是地堆疊起來(lái)的(所謂的“祖?zhèn)鞔a”),在這個(gè)過(guò)程中沒有人意識(shí)到有必要去維護(hù)軟件的概念完整性。而DDD,特別是DDD在戰(zhàn)略層面提供的概念,是維護(hù)軟件概念完整性的良藥。概念完整性對(duì)軟件開發(fā)的重要性在弗雷德里克·布魯克斯(FrederickP.BrooksJr.)的經(jīng)典著作《人月神話》一書中被重點(diǎn)提出。什么叫作概念完整性?通俗地講,就是所有人對(duì)領(lǐng)域內(nèi)的所有事物持相同的看法。舉例來(lái)說(shuō),我們把“張三”這個(gè)“對(duì)象”叫作張三后,就不會(huì)再把他叫作李四。我們不會(huì)把張三的兒子也叫作張三,我們可以把他叫作“張小三”。大家都知道張三和張小三之間存在父子關(guān)系,不會(huì)剛才有人說(shuō)他們是父子,轉(zhuǎn)臉又有人說(shuō)他們是兄弟。在《領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)精簡(jiǎn)版》的第5章中,介紹了DDD在戰(zhàn)略層面維護(hù)模型的概念完整性的方法。筆者個(gè)人認(rèn)為其中最重要的兩個(gè)概念是限界上下文(BoundedContext)與防腐層(Anti-CorruptionLayer),下文打算針對(duì)它們以及“統(tǒng)一語(yǔ)言”進(jìn)行闡述。其他戰(zhàn)略層面的概念讀者可自行閱讀《領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)精簡(jiǎn)版》或搜索其他文章以深入了解,在此不再贅述。1.4.1限界上下文為了開發(fā)軟件,我們需要分析應(yīng)用要解決的領(lǐng)域問(wèn)題的范圍,捕獲那些重要的概念,給它們明確的定義——通俗地說(shuō),一個(gè)“說(shuō)法”(概念)不要指兩個(gè)“東西”,當(dāng)然一個(gè)“東西”也不要有兩個(gè)“說(shuō)法”。我們使用這些概念來(lái)構(gòu)建模型,反映我們對(duì)領(lǐng)域的認(rèn)知。限界上下文(BoundedContext),顧名思義就是限定了邊界的上下文,它定義了每個(gè)模型的應(yīng)用范圍。在本書的行文中,經(jīng)常會(huì)把限界上下文簡(jiǎn)稱為“上下文”。怎么理解“上下文”這個(gè)說(shuō)法?為什么不叫作“子系統(tǒng)”“模塊”?這么說(shuō)吧,很多詞語(yǔ)都可能存在多個(gè)含義,它到底是哪個(gè)意思,往往需要聯(lián)系上下文才能判斷。比如,當(dāng)我們說(shuō)起“Good”這個(gè)單詞時(shí),它到底是“貨物”,還是“好的東西”“好的品質(zhì)”呢?它到底是名詞,還是形容詞呢?如果它不是出現(xiàn)在一個(gè)上下文中——比如某篇文章的某一段的某個(gè)句子中,我們其實(shí)無(wú)從判斷。限界上下文就是這樣的一個(gè)上下文、一個(gè)邊界,在這個(gè)邊界內(nèi),所有重要的術(shù)語(yǔ)、詞語(yǔ)都有一個(gè)明確的解釋。你看,上下文這個(gè)說(shuō)法是不是比“子系統(tǒng)”“模塊”之類的說(shuō)法要貼切?有人可能會(huì)提出這樣的問(wèn)題:限界上下文多大才合適,劃分上下文有沒有什么可以遵循的規(guī)則?劃分上下文的規(guī)則,無(wú)非還是放之四海而皆準(zhǔn)的“高內(nèi)聚,低耦合”,這么說(shuō)估計(jì)大家都會(huì)覺得太虛。其實(shí)真正讓大家感到糾結(jié)、不知如何切分的那些東西之間一定有所關(guān)聯(lián),那就干脆都納入一個(gè)上下文中!與其關(guān)注上下文的“大小”,不如關(guān)注模型的“質(zhì)量”——概念完整性有沒有被破壞。筆者認(rèn)為:判斷大小是不是合適,要看應(yīng)用的開發(fā)團(tuán)隊(duì)能在一個(gè)多大的范圍內(nèi)掌控軟件的概念完整性,只要開發(fā)團(tuán)隊(duì)沒有問(wèn)題,那么這個(gè)范圍就算再大,作為一個(gè)上下文來(lái)處理都是可以的。1.4.2限界上下文與微服務(wù)那么,限界上下文與微服務(wù)架構(gòu)\h[1](MSA)中的那個(gè)微服務(wù)之間有什么關(guān)系?有人還使用過(guò)“物理限界上下文”這個(gè)術(shù)語(yǔ),甚至把一個(gè)可獨(dú)立部署的微服務(wù)稱為物理限界上下文,這個(gè)叫法合理嗎?讓我們來(lái)設(shè)想一下,現(xiàn)在要給一個(gè)餐廳開發(fā)一個(gè)在線外賣(Takeout)應(yīng)用。我們使用了微服務(wù)架構(gòu),決定在后端開發(fā)兩個(gè)可以獨(dú)立部署的微服務(wù):·OrderService。與訂單管理相關(guān)的服務(wù),它與前端App進(jìn)行交互,處理消費(fèi)者的下單請(qǐng)求?!itchenService。處理后廚相關(guān)的業(yè)務(wù)邏輯。事實(shí)上要?jiǎng)?chuàng)建一個(gè)真實(shí)的外賣應(yīng)用,可能要處理的后端業(yè)務(wù)邏輯遠(yuǎn)比這里列出的多,比如可能我們還需要用ConsumerService來(lái)處理消費(fèi)者的信息,需要用AccountingService來(lái)處理支付邏輯等。這里為了簡(jiǎn)化問(wèn)題,先忽略它們。假設(shè)這兩個(gè)微服務(wù)是以發(fā)布/訂閱事件的方式——也就是使用所謂的基于協(xié)作的Saga(Choreography-basedSaga)——來(lái)完成業(yè)務(wù)事務(wù)的,“下單”事務(wù)處理的正常路徑如下:1)OrderServcie接收到客戶端App的下單請(qǐng)求,創(chuàng)建一個(gè)處于PENDING狀態(tài)的訂單,然后發(fā)布OrderCreated(訂單已創(chuàng)建)事件。2)KitchenService訂閱、消費(fèi)OrderCreated事件,驗(yàn)證訂單信息,檢查后廚的食材、物料、人員等信息,創(chuàng)建后廚工單(Ticket),并發(fā)布TicketCreated(后廚工單已創(chuàng)建)事件。3)OrderService訂閱、消費(fèi)TicketCreated事件,將訂單狀態(tài)置為APPROVED,并發(fā)布OrderApproved事件。假設(shè)開發(fā)人員編寫了以上事件(OrderCreated、TicketCreated)以及其他領(lǐng)域?qū)ο蟮撵o態(tài)類型——Java開發(fā)人員稱之為POJO,.NET開發(fā)人員稱之為POCO,PHP開發(fā)人員稱之為POPO——的代碼,然后把這些代碼都放到一個(gè)叫作common-api的類庫(kù)項(xiàng)目中——我們可以構(gòu)建這個(gè)項(xiàng)目,打包出jar、dll、phar之類格式的歸檔包文件,供其他項(xiàng)目使用。另外兩個(gè)“服務(wù)”項(xiàng)目OrderService和KitchenService都依賴這個(gè)common-api類庫(kù)項(xiàng)目,它們會(huì)直截了當(dāng)?shù)厥褂美锩娴撵o態(tài)類型,使用它們的時(shí)候并不會(huì)經(jīng)過(guò)什么轉(zhuǎn)換,而且會(huì)毫不在意地對(duì)其他部分(比如客戶端)暴露它們的名字和概念?,F(xiàn)在,請(qǐng)問(wèn)我們是有“一個(gè)”限界上下文還是有“兩個(gè)”限界上下文?有些講述在微服務(wù)架構(gòu)中實(shí)踐DDD的文章,對(duì)于這樣的情形會(huì)判斷為兩個(gè)限界上下文,每個(gè)微服務(wù)對(duì)應(yīng)一個(gè)上下文。從限界上下文原本的概念出發(fā),筆者認(rèn)為這里只存在一個(gè)上下文。在這個(gè)例子里,兩個(gè)服務(wù)都依賴同一個(gè)領(lǐng)域?qū)ο蟮念悗?kù),大家都說(shuō)統(tǒng)一的語(yǔ)言(UbiquitousLanguage),使用同一套術(shù)語(yǔ)——OrderCreated、TicketCreated等——來(lái)進(jìn)行溝通,彼此之間并沒有什么隔閡,顯然大家都處于同一個(gè)限界上下文中。限界上下文是概念的邊界,微服務(wù)是物理的軟件組件。微服務(wù)架構(gòu)(MSA)在實(shí)踐中可能會(huì)產(chǎn)生很多細(xì)粒度的微服務(wù),這些微服務(wù)的物理邊界與DDD的聚合邊界或領(lǐng)域服務(wù)的邊界對(duì)齊是比較常見的情況。如果一個(gè)微服務(wù)與其他微服務(wù)共享很多相同的概念,那么雖然它具有獨(dú)立的物理邊界,也很難稱得上是一個(gè)限界上下文。\h[1]見/articles/microservices.html。1.4.3防腐層我們創(chuàng)建的應(yīng)用總是免不了要與外部的應(yīng)用發(fā)生交互。與外部應(yīng)用交互時(shí),應(yīng)該考慮使用六邊形架構(gòu)\h[1]風(fēng)格。在傳統(tǒng)的分層架構(gòu)風(fēng)格中,上層的軟件構(gòu)造塊(元素)依賴于下層的構(gòu)造塊。六邊形架構(gòu)風(fēng)格摒棄了這種觀點(diǎn)。它認(rèn)為,一個(gè)軟件構(gòu)造塊如果需要與外部軟件元素進(jìn)行交互來(lái)實(shí)現(xiàn)功能、完成自己的使命,那么它也只應(yīng)該依賴于抽象。這個(gè)抽象就是它對(duì)外部軟件元素的期望、設(shè)想。這其實(shí)也是依賴倒置原則\h[2]的應(yīng)用。擴(kuò)展一下上面的在線外賣應(yīng)用的例子。假設(shè)為了實(shí)現(xiàn)這個(gè)應(yīng)用,我們還需要兩個(gè)服務(wù)參與其中:·ConsumerService(消費(fèi)者服務(wù)),它的職責(zé)是處理對(duì)消費(fèi)者信息的查詢和驗(yàn)證邏輯?!eliveryService(送餐服務(wù)),這個(gè)服務(wù)負(fù)責(zé)送餐業(yè)務(wù)流程的管理。這兩個(gè)服務(wù)真正的業(yè)務(wù)邏輯可能并不需要外賣應(yīng)用的開發(fā)團(tuán)隊(duì)去實(shí)現(xiàn)。也許送餐本來(lái)就是由第三方公司提供的服務(wù),對(duì)方已經(jīng)有了管理送餐業(yè)務(wù)的軟件(下面假設(shè)這個(gè)軟件叫送餐系統(tǒng));也許有些業(yè)務(wù)是公司內(nèi)其他部門的團(tuán)隊(duì)負(fù)責(zé)的,并且已經(jīng)有了“現(xiàn)成的”系統(tǒng),比如ConsumerService需要的信息在公司內(nèi)的CRM系統(tǒng)中本來(lái)就存在。不管怎么說(shuō),現(xiàn)在“送餐服務(wù)”和“消費(fèi)者服務(wù)”的業(yè)務(wù)邏輯都不需要外賣應(yīng)用的開發(fā)團(tuán)隊(duì)去實(shí)現(xiàn)了,大家要做的只是把已有的應(yīng)用與外賣應(yīng)用進(jìn)行集成??傊?,不管是外部的送餐系統(tǒng)還是公司已經(jīng)存在的CRM系統(tǒng),都不在外賣應(yīng)用開發(fā)團(tuán)隊(duì)控制的范圍之內(nèi),它們是另外兩個(gè)上下文。但是外賣應(yīng)用仍然可以提出它希望這些外部服務(wù)“看起來(lái)是什么樣子的”,比如:·它希望ConsumerService有一個(gè)verifyConsumer方法。調(diào)用這個(gè)方法可以驗(yàn)證某些消費(fèi)者的信息是否有效?!にM鸇eliveryService有一個(gè)scheduleDelivery方法。若使用訂單信息作為參數(shù)調(diào)用這個(gè)方法,就可以請(qǐng)求送餐服務(wù)的提供者對(duì)訂單進(jìn)行派送?!にM筒头?wù)的提供者“知道”它提供了一個(gè)叫作NoteOrderDelivered的通知接口。在送餐員把食物送到消費(fèi)者手中之后,它希望這個(gè)接口被調(diào)用,且它能收到一個(gè)類型為OrderDelivered(訂單已完成派送)的事件,然后根據(jù)事件信息做進(jìn)一步的處理。外賣應(yīng)用對(duì)外部系統(tǒng)的這些期望都是“單相思”。它才不管外部的CRM系統(tǒng)有沒有一個(gè)名字叫作verifyConsumer的方法呢,也許在CRM里面根本就沒有Consumer這個(gè)概念,只有Customer的概念。它也不管在送餐員完成派送后送餐系統(tǒng)能不能發(fā)布一個(gè)類型為OrderDelivered的事件,也許在送餐系統(tǒng)里面根本沒有Order的概念,只有Delivery的概念,類似的事件叫作DeliveryCompleted。外賣應(yīng)用不管這些,它就要說(shuō)“Consumer”“OrderDelivered”,而不管外部系統(tǒng)的叫法。它把“想要的東西”都按照自己的想法定義了接口,也就是說(shuō),現(xiàn)在ConsumerService和DeliveryService都是外賣應(yīng)用定義的接口——這就是所謂的“依賴于抽象,不依賴于實(shí)現(xiàn)”。外賣應(yīng)用的這些抽象,都屬于限界上下文的一部分。在自己的上下文內(nèi),只要能保證概念完整性即可,外賣應(yīng)用沒有什么理由改變自己,保持純粹就挺好的。如果外賣應(yīng)用未能抵制住誘惑,讓外部系統(tǒng)的概念滲透進(jìn)了內(nèi)部,比如在代碼里面一會(huì)兒稱客人為Consumer,一會(huì)兒又叫人家Customer——如此不正經(jīng),那就是“腐化”墮落。如果外部的CRM系統(tǒng)和送餐系統(tǒng)也不想改變自己,那么矛盾如何解決?怎樣把三觀不同的系統(tǒng)“整”到一起?這就需要防腐層(Anti-CorruptionLayer)在中間牽線搭橋、互相撮合了。對(duì)于采用六邊形架構(gòu)的應(yīng)用來(lái)說(shuō),防腐層一般是作為適配器來(lái)實(shí)現(xiàn)的(如圖1-1所示)。六邊形架構(gòu)的適配器是限界上下文的核心業(yè)務(wù)邏輯組件與外部系統(tǒng)之間的交互接口(又稱為端口)的實(shí)現(xiàn)。適配器分兩類:出站適配器與入站適配器。一個(gè)是適配器是“出站”還是“入站”是站在核心業(yè)務(wù)邏輯組件的角度看的。如果交互是請(qǐng)求/響應(yīng)模式的調(diào)用,出站適配器實(shí)現(xiàn)對(duì)外部系統(tǒng)的調(diào)用,入站適配器處理外部系統(tǒng)的請(qǐng)求、調(diào)用內(nèi)部的業(yè)務(wù)邏輯。如果交互是異步消息通信,那么出站適配器實(shí)現(xiàn)對(duì)外發(fā)送消息,而入站適配器接收外部發(fā)送過(guò)來(lái)的消息。圖1-1六邊形架構(gòu)與防腐層假設(shè),外賣應(yīng)用上下文在使用CRM或送餐系統(tǒng)提供的服務(wù)時(shí)屬于“弱勢(shì)客戶”,那么外賣應(yīng)用的開發(fā)團(tuán)隊(duì)就需要自己構(gòu)造防腐層,防止“強(qiáng)勢(shì)的供應(yīng)商”(CRM上下文或送餐系統(tǒng)上下文)一方的概念侵入自己的上下文中。在圖1-1中,外賣應(yīng)用的開發(fā)人員最終實(shí)現(xiàn)了三個(gè)適配器:·為ConsumerService接口實(shí)現(xiàn)了一個(gè)出站適配器CRMConsumerServiceAdaptor?!镈eliveryService接口實(shí)現(xiàn)了一個(gè)出站適配器3rdPartyDeliveryServiceAdaptor?!镹oteOrderDelivered通知接口實(shí)現(xiàn)一個(gè)入站適配器3rdPartyDeliveryServiceNotifier。防腐層的代碼就位于這三個(gè)適配器的內(nèi)部。我們?cè)趫D1-1中用虛線劃出了三個(gè)限界上下文的邊界。只有那些包含防腐層代碼的適配器是橫跨不同的限界上下文的邊界的,它們負(fù)責(zé)在不同上下文之間對(duì)概念進(jìn)行翻譯和轉(zhuǎn)換。雖然外賣應(yīng)用最終(在運(yùn)行時(shí))還是需要使用這些適配器才能實(shí)現(xiàn)完整的功能,但是就代碼的依賴關(guān)系而言,是適配器依賴外賣應(yīng)用定義的那些接口——這些接口又叫抽象,它們屬于外賣應(yīng)用核心業(yè)務(wù)邏輯組件的一部分。開發(fā)人員都知道這一點(diǎn):當(dāng)一個(gè)類實(shí)現(xiàn)一個(gè)接口時(shí),依賴關(guān)系是類依賴接口,而不是接口依賴類??傊?,限界上下文是需要時(shí)刻保護(hù)好的概念的邊界。需要將某個(gè)上下文的概念轉(zhuǎn)換為另一個(gè)上下文概念的地方就是防腐層。最好讓防腐層的代碼和其他軟件元素(構(gòu)造塊)之間存在明顯的物理邊界,倒不是說(shuō)一定要把防腐層部署為一個(gè)個(gè)獨(dú)立的服務(wù)或在獨(dú)立的進(jìn)程中運(yùn)行,但是,最少我們還可以考慮將一個(gè)防腐層作為獨(dú)立的類庫(kù)項(xiàng)目進(jìn)行構(gòu)建和維護(hù)。\h[1]Hexagonalarchitecture(software),/wiki/Hexagonalarchitecture(software)。\h[2]DependencyInversionPrinciple,/wiki/Dependencyinversionprinciple。1.4.4統(tǒng)一語(yǔ)言領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)的一個(gè)核心原則是使用一種基于模型的語(yǔ)言——統(tǒng)一語(yǔ)言(UbiquitousLanguage)。統(tǒng)一語(yǔ)言使用模型作為語(yǔ)言的主干,團(tuán)隊(duì)在進(jìn)行所有的交流時(shí)都應(yīng)該使用它。在共享知識(shí)和推敲模型時(shí),團(tuán)隊(duì)在PPT、文字和圖形中使用它。程序員在代碼中也要使用它。為了創(chuàng)建團(tuán)隊(duì)的統(tǒng)一語(yǔ)言,我們需要發(fā)現(xiàn)領(lǐng)域和模型的那些關(guān)鍵概念(其中有些概念可能很不容易被發(fā)現(xiàn)),并找到適合描述它們的詞匯,然后開始盡可能多地使用它們。統(tǒng)一語(yǔ)言為何如此重要?眾所周知,在復(fù)雜軟件系統(tǒng)的開發(fā)過(guò)程中,大量甚至是大部分的時(shí)間和資源都消耗在思想的溝通和確認(rèn)上了。統(tǒng)一語(yǔ)言基于“模型”,精心構(gòu)建的模型歸納總結(jié)了各方對(duì)領(lǐng)域的一致認(rèn)知,統(tǒng)一的“說(shuō)法”可避免歧義、減少溝通中的誤解。所以,統(tǒng)一語(yǔ)言可以成為各方(業(yè)務(wù)人員、領(lǐng)域?qū)<?、產(chǎn)品人員、開發(fā)人員、測(cè)試人員)進(jìn)行高效溝通的基礎(chǔ),從而極大地節(jié)約軟件開發(fā)的成本。在實(shí)踐上,我們建議使用一個(gè)簡(jiǎn)潔的詞匯表來(lái)記錄一個(gè)上下文的統(tǒng)一語(yǔ)言中的那些關(guān)鍵概念。比如,表1-1是GitH的reactive-streams/reactive-streams-jvm\h[1]代碼庫(kù)中一個(gè)詞匯表的一部分。表1-1一個(gè)英文詞匯表的示例對(duì)于主要由母語(yǔ)為中文的開發(fā)人員組成的團(tuán)隊(duì)來(lái)說(shuō),建議在軟件開發(fā)項(xiàng)目中使用中英文對(duì)照的詞匯表,并且建議在詞匯表中強(qiáng)調(diào)詞條的詞性(名字、動(dòng)詞、形容詞等),甚至名詞的單復(fù)數(shù)形式等。因?yàn)樵谥形闹泻芏嘣~語(yǔ)的詞性都是模糊和多變的,需要根據(jù)上下文去辨別,母語(yǔ)為中文的開發(fā)人員在文檔和代碼中使用錯(cuò)誤的詞性的情況實(shí)在太過(guò)常見。表1-2是中英文對(duì)照的詞匯表的一個(gè)例子,也許可供借鑒。表1-2一個(gè)中英文對(duì)照詞匯表的示例\h[1]ReactiveStreamsSpecificationfortheJVM,/reactive-streams/reactive-streams-jvm。1.5ER模型、OO模型和關(guān)系模型我們可能有必要先了解一下在軟件開發(fā)活動(dòng)中常見的這幾種模型,或者說(shuō)模型范式,如下:·ER模型(實(shí)體關(guān)系模型)。讀者如果有興趣,可以通過(guò)Wikipedia了解一下ER模型\h[1],以及相關(guān)的概念數(shù)據(jù)模型、邏輯數(shù)據(jù)模型、物理數(shù)據(jù)模型等?!O模型(面向?qū)ο竽P停_@可能是大多數(shù)開發(fā)人員最熟悉的一種程序設(shè)計(jì)模型了。·關(guān)系模型。這是我們常見的關(guān)系型數(shù)據(jù)庫(kù)系統(tǒng)(RDBS)所采用的模型,大部分應(yīng)用軟件使用的數(shù)據(jù)庫(kù)都是關(guān)系型數(shù)據(jù)庫(kù)。這幾種模型都有一套自己的概念和術(shù)語(yǔ),其中有些概念存在大致的對(duì)應(yīng)關(guān)系,我們可以用圖1-2來(lái)大致表示。圖1-2ER模型、OO模型與關(guān)系模型其實(shí)關(guān)系模型使用的“原汁原味”的術(shù)語(yǔ)是以下形式?!りP(guān)系(Relation):一個(gè)關(guān)系對(duì)應(yīng)著一個(gè)二維表?!ぴM(Tuple):在二維表中的一行,稱為一個(gè)元組?!傩裕ˋttribute):在二維表中的列,稱為屬性。為了照顧大多數(shù)開發(fā)人員的習(xí)慣,書中還是采用了“表”“行”“列”這樣的術(shù)語(yǔ)。模型是用來(lái)反映對(duì)領(lǐng)域的認(rèn)知的,對(duì)于同樣的認(rèn)知,我們其實(shí)可以使用不同的方式來(lái)表述。因?yàn)槲覀兊哪繕?biāo)是開發(fā)軟件,所以最好是使用一些有利于后面編碼“實(shí)現(xiàn)”的表述方式。還是舉個(gè)例子吧,假設(shè)我們對(duì)現(xiàn)實(shí)世界(領(lǐng)域)的認(rèn)知是這樣的:·寵物(Pet)有性別(Gender),有自己的主人(Owner),假設(shè)一只寵物只有一個(gè)主人?!ひ粋€(gè)人(Person)可以擁有多只寵物。我們把這些信息(認(rèn)知)使用不同范式的模型來(lái)描述,結(jié)果可能大致如圖1-3所示。圖1-3分別使用ER模型、OO模型與關(guān)系模型表述同樣的認(rèn)知·對(duì)于關(guān)系模型,Pet表中存在Gender和Owner_Id列;Person表中存在Person_Id列,這一列是Person表的主鍵(PK)。Pet表中存在一個(gè)外鍵,可以把這個(gè)外鍵命名為PET_OWNER,它從Pet表的Owner_Id列指向Person表的Person_Id列?!?duì)于ER模型,有一個(gè)實(shí)體叫作Pet,它有一個(gè)屬性叫作Gender;還有一個(gè)實(shí)體叫作Person。從Person實(shí)體到Pet存在一個(gè)“一對(duì)多”的關(guān)系,這個(gè)關(guān)系的名稱叫作Has(有),即一個(gè)人可以擁有多只寵物的意思。·對(duì)于OO模型,有個(gè)類叫作Pet,它除了有一個(gè)Gender屬性,還有一個(gè)Owner屬性,后者的類型是Person,指向?qū)櫸锏闹魅?。而類Person有一個(gè)屬性叫作Pets,它的類型是Pet的集合(在圖1-3中記作Pet[]),指向一個(gè)人可以擁有的寵物(Pets)。Person的Pets屬性與Pet的Owner屬性其實(shí)描述的是同一個(gè)關(guān)系??匆粋€(gè)通俗的示例,無(wú)論是說(shuō)“張小三的爸爸是張三”,還是說(shuō)“張三有個(gè)兒子叫張小三”,都表示“張三與張小三存在父子關(guān)系”。\h[1]Entity–relationshipmodel(ERmodel),/wiki/Entity%E2%80%93relationship_model。1.6概念建模與模型范式有人認(rèn)為,在將現(xiàn)實(shí)世界映射到信息世界的第一階段,即概念建模階段,適宜使用ER模型。對(duì)此我們有不同看法。實(shí)際上不管是ER模型、OO模型還是關(guān)系模型,用來(lái)做概念建模都沒有問(wèn)題。所謂的概念建模,主要是選擇忽略領(lǐng)域中那些相對(duì)不太重要的細(xì)節(jié),只留下最關(guān)鍵的部分,這與使用的模型范式無(wú)關(guān)。ER模型和關(guān)系模型的一些概念本來(lái)就存在較好的對(duì)應(yīng)關(guān)系。關(guān)系數(shù)據(jù)庫(kù)的流行已經(jīng)證明了關(guān)系模型的表述能力。在實(shí)踐中,在概念建模階段使用二維表(關(guān)系模型)來(lái)展示領(lǐng)域中的關(guān)鍵概念以及概念之間的聯(lián)系,很多時(shí)候?qū)Ψ羌夹g(shù)人員來(lái)說(shuō)都是可以理解和接受的,甚至對(duì)他們來(lái)說(shuō)是很“親切”的。我們經(jīng)??吹胶芏鄻I(yè)務(wù)人員(軟件的最終用戶)、業(yè)務(wù)部門制作的Excel極其復(fù)雜,我不認(rèn)為他們理解關(guān)系模型有太大的難度。而用于操作關(guān)系型數(shù)據(jù)庫(kù)的SQL(結(jié)構(gòu)化查詢語(yǔ)言)一開始也并非是專門面向技術(shù)人員而設(shè)計(jì)的。UML\h[1](通用建模語(yǔ)言)作為一門“通用”語(yǔ)言,其野心自然是覆蓋從概念建模到實(shí)現(xiàn)建模的需要。UML在設(shè)計(jì)上傾向于滿足使用OO范式進(jìn)行建模的需要,大家一直都在使用UML進(jìn)行概念建模(構(gòu)建概念OO模型)。而DDD的領(lǐng)域模型就是基于OO模型的。它是在OO模型的基礎(chǔ)上進(jìn)一步抽象的,它定義了一些更高層次的概念,比如聚合、聚合根、服務(wù)、Factory、Repository等,所以在概念建模階段使用DDD領(lǐng)域模型也毫無(wú)問(wèn)題。實(shí)際上,DDD關(guān)注的重點(diǎn)就是在概念建模階段產(chǎn)出那個(gè)概念上的“領(lǐng)域模型”,只是DDD強(qiáng)調(diào)在做概念建模的時(shí)候要朝著軟件的實(shí)現(xiàn)方向“睜一只眼”(withaneyeopen)。如果有一門DDD專用的建模語(yǔ)言,估計(jì)大家也會(huì)期望它能覆蓋從概念建模到實(shí)現(xiàn)建模的需要吧!\h[1]見/wiki/UML。第2章其他DDD相關(guān)概念在認(rèn)識(shí)或溫習(xí)了DDD在戰(zhàn)略以及戰(zhàn)術(shù)層面的部分關(guān)鍵概念之后,我們還需要進(jìn)一步了解其他一些相關(guān)的概念。因?yàn)樵趯?shí)踐DDD的過(guò)程中我們會(huì)經(jīng)常使用這些概念,它們也很重要,而在其他DDD圖書中對(duì)它們強(qiáng)調(diào)得可能還不夠。這些概念包括領(lǐng)域ID、LocalID、GlobalID、命令、事件、狀態(tài)等。2.1領(lǐng)域ID根據(jù)DDD對(duì)實(shí)體的定義來(lái)看,實(shí)體必然存在一個(gè)ID,我們可以把這個(gè)ID稱為實(shí)體的領(lǐng)域ID。相信很多人都想問(wèn)以下問(wèn)題:·這個(gè)領(lǐng)域ID是不是應(yīng)該映射到關(guān)系模型的自然鍵?如果一個(gè)實(shí)體的ID已經(jīng)是“自然鍵”了,那么與之對(duì)應(yīng)的關(guān)系數(shù)據(jù)庫(kù)的Table中還有必要再引入這個(gè)ID之外的代理鍵嗎?·如果領(lǐng)域ID可以是代理鍵,那么它什么時(shí)候應(yīng)該是代理鍵?·在某些軟件系統(tǒng)上,開發(fā)人員會(huì)不分青紅皂白地給每個(gè)表(實(shí)體)設(shè)計(jì)一個(gè)代理主鍵。那么,在代碼中這樣重度地使用代理鍵是合理的做法嗎?·如果一個(gè)實(shí)體需要被其他實(shí)體引用,其他實(shí)體是不是應(yīng)該盡可能統(tǒng)一地通過(guò)持有它的領(lǐng)域ID的值來(lái)引用(指向)它?接下來(lái)我們討論一下這些問(wèn)題。2.1.1自然鍵與代理鍵什么是自然鍵(NaturalKey)?先看看維基百科的定義:在關(guān)系模型數(shù)據(jù)庫(kù)的設(shè)計(jì)中,自然鍵是指由現(xiàn)實(shí)世界中已經(jīng)存在的屬性所構(gòu)成的“鍵”。舉個(gè)例子,美國(guó)公民的社會(huì)保險(xiǎn)號(hào)碼可以用作自然鍵。換句話來(lái)說(shuō),自然鍵是與那一“行”中的屬性存在邏輯關(guān)系的候選鍵。自然鍵有時(shí)候也被稱為領(lǐng)域鍵。從這個(gè)定義可知,自然鍵有時(shí)候也被稱為領(lǐng)域鍵,領(lǐng)域鍵和領(lǐng)域ID這兩個(gè)稱呼都帶著“領(lǐng)域”,已經(jīng)很接近了,不是嗎?我們看到,這個(gè)定義還依賴于另外一個(gè)概念:現(xiàn)實(shí)世界(TheRealWorld)。那么,什么是現(xiàn)實(shí)世界?如果這個(gè)概念沒有定義,那么自然鍵的概念還是“不清不楚”的。雖然在討論領(lǐng)域模型的時(shí)候,我們更愿意使用實(shí)體ID這個(gè)說(shuō)法,而不是使用關(guān)系數(shù)據(jù)庫(kù)中“鍵”的概念,但是在不太嚴(yán)格的情況下,有時(shí)候我們也會(huì)混用實(shí)體ID和(自然)鍵這兩個(gè)說(shuō)法。筆者曾經(jīng)在Google上搜索到一篇文章ChoosingaPrimaryKey:NaturalorSurrogate?\h[1],也許,我們可以通過(guò)文章中的以下描述理解什么是TheRealWorld(中文翻譯版本):不要自然化代理鍵。一旦你向最終用戶顯示了代理鍵的值,或者更壞的是允許他們使用該值(例如搜索該值),實(shí)際上你已經(jīng)給它們賦予了業(yè)務(wù)含義。這實(shí)際上是自然化了代理鍵,從而失去了代理鍵的優(yōu)點(diǎn)。根據(jù)這個(gè)說(shuō)法,我們可以認(rèn)為,使用軟件的最終用戶所生活的世界就是現(xiàn)實(shí)世界,這個(gè)世界沒有代理鍵的容身之地。這么說(shuō)來(lái),代理鍵只應(yīng)該活在技術(shù)人員的世界里?總之,能不能見“人”(指的是最終用戶),是筆者能找到的用于區(qū)別自然鍵和代理鍵的最不讓人困惑的標(biāo)準(zhǔn)了。\h[1]見/essays/keys.html。2.1.2DDD實(shí)體的ID需要被最終用戶看到DDD所說(shuō)的實(shí)體的ID應(yīng)不應(yīng)該見“人”(最終用戶)呢?顯然,實(shí)體ID有99.99%的可能性是需要見“人”的。這個(gè)好像已經(jīng)不需要再展開討論了吧?因?yàn)轭I(lǐng)域模型應(yīng)該是由技術(shù)人員與領(lǐng)域?qū)<夜餐瑓⑴c構(gòu)建的,而領(lǐng)域?qū)<液芸赡芫褪亲罱K用戶。實(shí)體與值對(duì)象最本質(zhì)的區(qū)別就是實(shí)體存在ID,這么至關(guān)重要的事情,可不適合對(duì)領(lǐng)域?qū)<也刂粗??也許在軟件的內(nèi)部實(shí)現(xiàn)中,技術(shù)人員確實(shí)會(huì)使用一些不會(huì)被“軟件所服務(wù)的領(lǐng)域”的最終用戶看到的實(shí)體,但這些實(shí)體是“非主流”,而且對(duì)于這些實(shí)體來(lái)說(shuō),“技術(shù)領(lǐng)域”就是它服務(wù)的領(lǐng)域,技術(shù)人員就是它的最終用戶,所以它還是被最終用戶看到了。如果實(shí)體的ID需要被最終用戶看到,那么,在將領(lǐng)域模型映射到關(guān)系模型時(shí),實(shí)體ID的對(duì)應(yīng)物就必然是“自然鍵”。雖然Domain-DrivenDesign:TacklingComplexityintheHeartofSoftware一書中對(duì)此有不同的看法,但是筆者仍然堅(jiān)持自己的觀點(diǎn)。簡(jiǎn)單總結(jié)一下上面的論證過(guò)程:根據(jù)DDD的基本概念可知,有沒有ID是實(shí)體和值對(duì)象的本質(zhì)區(qū)別。沒有ID,實(shí)體這個(gè)概念就不存在了。而沒有實(shí)體,就無(wú)法進(jìn)行DDD領(lǐng)域模型在戰(zhàn)術(shù)層面的設(shè)計(jì)。至關(guān)重要的是,實(shí)體的ID要被最終用戶看見,所以它在數(shù)據(jù)庫(kù)層面對(duì)應(yīng)的東西就是自然鍵??梢?,一個(gè)實(shí)體只有代理鍵沒有自然鍵是不對(duì)的,

溫馨提示

  • 1. 本站所有資源如無(wú)特殊說(shuō)明,都需要本地電腦安裝OFFICE2007和PDF閱讀器。圖紙軟件為CAD,CAXA,PROE,UG,SolidWorks等.壓縮文件請(qǐng)下載最新的WinRAR軟件解壓。
  • 2. 本站的文檔不包含任何第三方提供的附件圖紙等,如果需要附件,請(qǐng)聯(lián)系上傳者。文件的所有權(quán)益歸上傳用戶所有。
  • 3. 本站RAR壓縮包中若帶圖紙,網(wǎng)頁(yè)內(nèi)容里面會(huì)有圖紙預(yù)覽,若沒有圖紙預(yù)覽就沒有圖紙。
  • 4. 未經(jīng)權(quán)益所有人同意不得將文件中的內(nèi)容挪作商業(yè)或盈利用途。
  • 5. 人人文庫(kù)網(wǎng)僅提供信息存儲(chǔ)空間,僅對(duì)用戶上傳內(nèi)容的表現(xiàn)方式做保護(hù)處理,對(duì)用戶上傳分享的文檔內(nèi)容本身不做任何修改或編輯,并不能對(duì)任何下載內(nèi)容負(fù)責(zé)。
  • 6. 下載文件中如有侵權(quán)或不適當(dāng)內(nèi)容,請(qǐng)與我們聯(lián)系,我們立即糾正。
  • 7. 本站不保證下載資源的準(zhǔn)確性、安全性和完整性, 同時(shí)也不承擔(dān)用戶因使用這些下載資源對(duì)自己和他人造成任何形式的傷害或損失。

評(píng)論

0/150

提交評(píng)論