IT行業的發展瞬息萬變,新技術層出不窮,很多技術人員出于個人興趣、個人職業發展等考慮而選擇一些流行的新技術,他們會把各種復雜的架構模式、高精尖的技術都加入架構中,這增加了項目的復雜度、延長了交付周期、增加了項目的研發成本。有些技術并不符合公司的情況,最后項目失敗了,某些技術人員就拿著“精通某某流行技術”的簡歷去找下家了,給公司留下一地雞毛。
因此,我們做架構設計的時候,一定要分析行業情況、公司情況、公司未來發展、項目情況、團隊情況等來設計適合自己的架構,不能盲目跟風。
能越來越多,淘寶網才逐漸進化到現在這樣復雜的架構。而現在很多網站在開發第一版的時候就以“上億人訪問,百萬并發量”為架構設計目標,導致項目遲遲無法交付、研發成本高昂,好不容易網站開發完成了,但是由于項目交付延 遲,公司已經錯過了絕佳的市場機會,上線后才幾千個注冊用戶,最后網站無疾而終。按照“精益創業”的理念,我們應該用最低的成本、最短的時間開發出一個“最小的可行性產品”?,然后把產品投入市場,根據市場的反饋再進行產品的升級。這里并不是讓讀者開發一個新產品的時候,也像淘寶網一樣寫普通的PHP代碼、部署到普通服務器上。經過IT行業的發展,我們現在已經可以用非常低的成本、在很短的時間內構建一個可承擔較大訪問量的高可用系統。我們只要基于成熟的技術進行開發,并且對項目未來較短一段時間內的發展進行預測,在項目架構上做必要的準備就可以了,沒必要“想得太長遠”?。架構設計在滿足必要的可擴展性、隔離性的基礎上,要盡可能簡單。
.NET是一個可以很好地支撐演進式架構的技術平臺。在前期網站訪問量低、沒有專業運維人員的情況下,我們可以把用.NET開發的程序部署到單機Windows服務器上,隨著網站規模的擴大,我們可以在不修改代碼的情況下,把程序遷移到Linux+Docker的環境下;在網站訪問量低的時候,我們可以用內存作為緩存,隨著網站訪問量的增大,我們可以切換為使用Redis作為緩存;.NET的依賴注入讓我們可以替換服務的實現類,而不需要修改服務消費者的代碼。一個好的軟件架構應該是可以防止軟件退化的。軟件退化指的是在軟件升級的時候,隨著功能的增加和系統復雜度的提升,代碼的質量越來越差,系統的穩定性和可維護性等指標越來越差。一個退化中的軟件的明顯特征就是:軟件的第一個版本是代碼質量最高的版本,之后的版本中代碼質量越來越差。軟件的需求是不斷變更的,軟件的升級也是必然的,因此我們應該在進行架構設計的時候避免后續軟件需求變更導致軟件退化,并且在軟件的升級過程中,我們要適時地進行架構的升級,以保持高質量的軟件設計。如果我們在每次軟件升級的時候沒有及時地調整程序結構,而是在原有的程序結構上不斷地加入代碼,最終軟件就會退化。
9.2 DDD的基本概念
隨著IT行業的發展,傳統的單體結構項目已經無法滿足如今的軟件項目的要求,越來越多的項目采用微服務架構進行開發,DDD是一個很好的應用于微服務架構的方法論。本節中,我們將會對微服務和與DDD相關的概念進行講解。本節講解的與DDD相關的概念比較晦澀難懂,這也是DDD學習中比較高的門檻。如果只是學習DDD的概念而沒有了解如何在實踐中應用它,我們會感覺概念沒有落地;如果我們過早關注這些概念的落地,會導致我們對于概念的理解過于片面。在DDD的學習中,我們一般會經歷多次“從理論到實踐,在實踐中應用一段時間,再回到理論”這樣的過程,才會對于DDD的概念及實踐有螺旋式上升的認知。
在編寫本書的時候,作者曾經考慮在講解一個概念的時候,直接給出這個概念在實際項目中的技術落地,但是最后還是決定把概念的講解和技術落地分開來講,也就是作者會先在本節中把所有的相關概念介紹一遍,讓讀者對于這些概念有一個整體的認知,然后在9.3節中對這些概念的技術落地進行介紹,避免讀者在學習這些概念的時候立即陷入技術落地的細節,而無法對整個體系有宏觀的了解。
9.2.1 什么是微服務
傳統的軟件項目大部分都是單體結構,也就是項目中的所有代碼都放到同一個應用程序中,一般它們也都運行在同一個進程中,如圖9-1所示。
單體結構單體結構的項目有結構簡單、部署簡單等優點,但是有如下的缺點。·代碼之間耦合嚴重,代碼的可維護性低。·項目只能采用單一的語言和技術棧,甚至采用的開發包的版本都必須統一。·一個模塊的崩潰就會導致整個項目的崩潰。·我們只能整體進行服務器擴容,無法對其中一個模塊進行單獨的服務器擴容。·當需要更新某一個功能時,我們需要把整個系統重新部署一遍,這會導致新功能的上線流程變長。微服務架構把項目拆分為多個應用程序,每個應用程序單獨構建和部署,如圖9-2所示。
我們講過,架構應該是進化而來的,同樣微服務架構也應該是進化而來的。因此在進行系統架構設計的時候,我們應該認真思考“這個項目真的需要微服務架構嗎”?。如果經過思考后,我們仍然決定要采用微服務架構,那么也要再思考“能不能減少微服務的數量”?。第一個版本的項目可以只有幾個微服務,隨著系統的發展,當我們發現一個微服務中某個功能已經發展到可以獨立的程度時(比如某個功能被高頻訪問、某個功能經常被其他微服務訪問)?,我們再把這個功能拆分為一個微服務。總之,是否采用微服務及如何采用微服務,應該是仔細思考后的結果,我們不能盲目跟風。馬丁·福勒(Martin Fowler)[1]曾經提過“分布式第一定律”?,那就是“避免使用分布式”?,由此,作者提出“微服務第一定律”?,那就是“避免使用微服務,除非有充足的理由”?。
9.2.3 DDD為什么難學
在9.2.2小節中我們講到了,我們需要合理的架構設計來避免微服務的濫用,而DDD是一種很好的指導微服務架構設計的范式,就像面向對象設計模式是一種很好的指導面向對象編程的范式一樣。DDD是由埃里克·埃文斯(Eric Evans)在2004年提出來的,但是一直停留在理論層次,多年來的實際應用并不廣泛,直到2014年,馬丁·福勒與詹姆斯·劉易斯(James Lewis)共同提出了微服務的概念,人們才發現DDD是一種很好的指導微服務架構設計的模式。DDD的誕生早于微服務的誕生,DDD并不是為微服務而生的,DDD也可以用于單體結構項目的設計,但是在微服務架構中DDD能發揮出更大的作用。
DDD并不是一個技術,而是一種架構設計的指導原則;DDD不是一種強制性的規范,各個項目可以根據自己的情況進行個性化的設計。DDD就像烹飪中餐時“鹽少許、油少許”一樣讓人難以捉摸,而且DDD中的概念非常多,表述非常晦澀因此很多人都對DDD望而生畏。不同項目的行業情況、公司情況、團隊情況、業務情況等不同,因此DDD不能給我們一個拿來就能照著用的操作手冊。每個人、每個團隊對DDD的理解不同,如果說“一千個人心中就有一千個哈姆雷特”的話,那么也可以說“一千個人心中就有兩千個DDD”?,因為同一個人對DDD也可能在不同時期有著不同的理解。
很多開發人員把DDD當成一個技術,這是非常大的一個誤區。DDD是一種設計思想,它分為戰略設計和戰術設計兩個層次:DDD的戰略設計可以幫助公司的領導人進行團隊的劃分、人員的組織、產品線的規劃等,也可以幫助產品經理對產品功能進行規劃,還可以幫助架構師進行項目架構的規劃、技術棧的選擇等;DDD的戰術設計則是對公司全員進行DDD具體實施過程的指導。
不同的人對DDD的理解及對DDD概念落地的理解有所不同,并不存在絕對的錯與對,在情況A下成功的DDD實戰經驗放到情況B下可能就會失敗。正如古人所說“橘生淮南則為橘,生于淮北則為枳”?,讀者不要在眾多的對DDD解讀的文章中迷失,也不要執著于尋找根本就不存在的“DDD最佳實踐”?,而要認真聆聽各方的解讀,并且根據項目的自身情況來個性化地實現DDD的落地。只要讀者能夠用DDD很好地指導項目,那么該落地方案就是最優解。
很多開發人員學習DDD的時候感覺無從下手,主要原因就是他們把DDD當成一個整體去學習,從而找不到學習的“落腳點”?。無論是公司管理人員、業務人員、架構師還是開發人員,在學習DDD的時候,應該先從自己能夠把握的方面去學習DDD,隨著對DDD應用的深入再逐漸了解DDD的全貌。本書是寫給開發人員的,因此本書主要專注于與代碼編寫相關的DDD概念。本書在講解這些概念的時候會把它們和具體的實現代碼技術棧理解了DDD的概念之后,讀者就可以在自己所在的項目中用不同的技術棧去實踐DDD。需要特別注意的是,即使對于相同的技術棧,不同人落地DDD的方式也不同,不存在“正確答案”?,讀者可以在理解了某個落地方式之后,在項目中使用不同的方式落地。
那么到底什么是DDD呢?
DDD的英文全稱是domain driven design,翻譯成中文就是“領域驅動設計”?。這里的主干詞是“設計”?,也就是說DDD是一種設計思想。這里的形容詞是“領域驅動”?,那么什么是“領域”呢?領域其實指的就是業務,因此DDD其實就是一種用業務驅動的設計。傳統的軟件設計把業務和實現技術割裂,在系統的需求設計完成后,技術人員把業務人員描述的需求文檔轉換為代碼去實現,業務人員和技術人員對系統的理解并不完全匹配。隨著系統的升級,技術人員對代碼進行修改,業務人員和技術人員對系統的理解偏差越來越大,從而造成系統的擴展性、可維護性越來越差。而DDD則是指在項目 的全生命周期內,管理、產品、技術、測試、實施、運維等所有崗位的人員都基于對業務的相同理解來開展工作。技術人員在把業務落地為設計、代碼的時候,也直接把業務映射到代碼中,而不是用代碼去實現業務。DDD的核心理念就是所有人員站在用戶的角度、業務的角度去思考問題,而不是站在技術的角度去思考問題。
9.2.4 領域與領域模型
“領域”?(domain)是一個比較寬泛的概念,主要指的是一個組織做的所有事情,比如一家銀行做的所有事情就是銀行的領域。為了縮小討論問題的范圍,我們通常會把領域細分為多個“子領域”?(簡稱“子域”?)?,比如銀行的領域就可以劃分為“對公業務子域”?“對私業務子域”?“內部管理子域”等,子域還可以繼續劃分為更細粒度的子域,比如“對私業務子域”可以劃分為“柜臺業務子域”?“ATM(automated teller machine,自動柜員機)業務子域”?“網銀業務子域”等。劃分出子域之后,我們就能專注于子域內部的領域相關業務的處理。
領域(包含子域)可以按照功能劃分為核心域、支撐域、通用域。核心域指的是解決項目的核心問題的領域,支撐域指的是解決項目的非核心問題的領域,而通用域指的是解決通用問題的領域。核心域是和組織業務緊密相關的、個性化的領域,支撐域則具有組織特性,但不具有通用性,而通用域則是可以被很多其他領域復用的領域。
領域的劃分可以不限于技術相關的問題。舉個例子,對于一家手機公司來講,手機的研發、制造、銷售業務就屬于核心域,售后業務、財務業務就屬于支撐域,而保潔、保安則屬于通用域。領域劃分為不同類別后,我們就可以為不同的領域投入不同的資源:對于核心域我們要投入重點資源,對于通用域我們可以采購外部服務,比如很多公司的保潔人員都是外包的第三方服務公司提供的。一個公司對于領域的不同分類也決定了公司業務方向的不同。一家注重銷售的手機公司,可能手機都是從第三方采購的,只是把手機貼上自己的商標而已,對于這樣的公司來講,研發、制造業務就是通用域。
從軟件開發技術這個層面來講,領域的不同分類也決定了公司的研發重點。對于一家普通軟件公司來講,業務邏輯代碼屬于核心域,權限管理、日志模塊等屬于支撐域,而報表工具、工作流引擎等屬于可以從外部采購的通用域。但是對于一家提供云計算基礎服務的公司來講,服務器資源管理、安全監控等屬于核心域,云服務器SDK、技術文檔、沙箱環境、計費模塊等則屬于支撐域,而操作系統、數據庫等屬于通用域。對于一家想要通過研發自己的操作系統、數據庫系統從而最大化地利用服務器資源的云計算公司來講,操作系統、數據庫等就屬于支撐域甚至核心域了。
確定一個領域之后,我們就要對領域內的對象進行建模,從而抽象出模型的概念,這些領域中的模型就叫作領域模型(domain model)?。比如銀行的柜臺業務領域中,就有儲戶、柜員、賬戶等領域模型。建模是DDD中非常核心的事情,一旦定義出了領域模型,我們就可以用領域模型驅動項目的開發。使用DDD,我們在分析完產品需求后,就應該創建領域模型,而不是考慮如何設計數據庫和編寫代碼。使用領域模型,我們可以一直用業務語言去描述和構建系統,而不是使用技術人員的語言。
與領域模型對應的概念是“事務腳本”?(transactionscript)?,事務腳本是指使用技術人員的語言去描述和實現業務事務,說通俗一點兒就是沒有太多設計,沒有考慮可擴展性、可維護性,通過使用if、for等語句用流水賬的形式編寫代碼。如代碼9-1所示,?“柜員取款”業務的偽代碼就是一個典型的事務腳本。
代碼9-1 典型的事務腳本
在這段代碼中,我們檢查當前柜員是否擁有操作取款業務的權限,然后檢查賬戶的余額,最后完成扣款。包括作者在內的很多開發人員的職業生涯中都寫過這樣流水賬式的代碼。這樣的代碼可以滿足業務需求,而且編寫簡單、自然,非常符合開發人員的思維方式。事務腳本代碼的問題在于,本應該屬于支撐域中的權限的概念出現在了核心域的代碼中,我們應該通過AOP(aspect-orientedprogramming,面向切面編程)等方式把權限校驗的代碼放到單獨的權限校驗支撐域中。這段代碼的另外一個問題是,它對于需求變更的響應是非常糟糕的,比如系統需要增加一個“取款金額大于5萬元需要主管審批”的功能,我們就要在第5行代碼之前加上一些if判斷語句;再比如系統需要增加一個取款成功后發送通知短信的功能,我們就要在第11行代碼之前加上發送短信的代碼……隨著系統需求的膨脹,Withdraw方法中可能膨脹出上千行代碼,代碼的可維護性、可擴展性非常差。
而根據領域模型、DDD開發完成的系統,代碼的可維護性、可擴展性會非常高。讀者可以在學習完本書后,嘗試重構這段代碼。
9.2.5 通用語言與界限上下文
在進行系統開發的時候,非常容易導致歧義的是不同人員 對于同一個概念的不同描述。比如用戶說“我想要商品可以被刪除”?,開發人員就開發了一個使用Delete語句把商品從數據庫中刪除的功能;后來用戶又說“我想把之前刪除的商品恢復回來”?,開發人員就會說“數據已經被刪除了,恢復不了”?,用戶就會生氣地說“Windows里的文件刪除后都能從回收站里恢復,你們刪除的怎么就恢復不了呢”?。這其實就是開發人員和用戶對于“刪除”這個詞語的理解不同造成的。再如,電商系統的支付模塊的開發人員和后臺管理模塊的開發人員聊了許久關于“用戶管理”的功能,最后才發現支付模塊開發人員說的“用戶”指的是購買商品的“客戶”?,而后臺管理模塊開發人員說的“用戶”指的是“網站管理員”?。
從上面兩個例子我們可以看出,在描述業務對象的時候,擁有確切含義的、沒有二義性的語言是非常重要的,這樣的語言就是“通用語言”?。在應用DDD的時候,團隊成員必須對于系統內的每一個業務對象有確定的、無二義性的、公認的定義。通用語言離不開特定的語義環境,只有確定了通用語言所在的邊界,才能沒有歧義地描述一個業務對象。比如,后臺管理模塊中的“用戶”和支付模塊的“用戶”就處于不同的邊界中,它們在各自的邊界內有著各自的含義。界限上下文就是用來確定通用語言的邊界的,在一個特定的界限上下文中,通用語言有著唯一的含義。
在學習DDD的時候,我們需要了解很多的名詞,比如領域、子域、實體類、值對象、聚合等,盡管這些概念比較晦澀難懂,但是所有學習和應用DDD的人員擁有同樣一套通用語言,所有人員都用同樣的通用語言進行表述和溝通,可以減少誤解。同時,DDD中的這些概念也是在DDD這個界限上下文中才有這些含義的,在其他界限上下文中可能就有其他含義了。
9.2.6 實體類與值對象
在DDD中,?“標識符”用來唯一定位一個對象,在數據庫中我們一般用表的主鍵來實現標識符。當談到標識符的時候,我們是站在業務的角度思考問題,而談到主鍵的時候,我們是站在技術的角度思考問題。
在DDD中大量存在著這樣一類對象,它們擁有唯一的標識符,標識符的值不會改變,而對象的其他狀態則會經歷各種變化,這樣的對象可能會被持久化地保存在存儲設備中,即使軟件重啟,我們也可以把持久化在存儲設備中的對象還原出來,我們把這樣的對象稱為實體類(entity)?。標識符是用來跟蹤對象狀態變化的,一個實體類的對象無論經歷怎樣的變化,只要看到標識符的值沒有變化,我們就知道它們還是那個對象。
在具體實現DDD的時候,實體類一般的表現形式就是EFCore中的實體類,實體類的Id屬性一般就是標識符,Id屬性的值不會變化,它標識著唯一的對象,實體類的其他屬性則可能在運行時被修改,但是只要Id不變,我們就知道前后兩個對象指的是同一個對象。我們可以把實體類的對象保存到數據庫中,也可以把它從數據庫中讀取出來。
在DDD中還存在著一些沒有標識符的對象,它們也有多個屬性,它們依附于某個實體類對象而存在,這些沒有標識符的對象叫作值對象。同一個值對象不會被多個實體類 對象引用;值對象一般是不可變的,也就是值對象的屬性不可以修改。因此如果我們要修改實體類中的一個值對象屬性,我們只能創建一個新的值對象來替換舊的值對象。
比如,在電子地圖系統中,?“商家”就是一個實體類,該實體類包含營業執照編號、名稱、經緯度位置、電話等屬性。一個商家的營業執照編號是不可以修改的,而商家的名稱、經緯度位置、電話都是可以修改的,只要兩個商家的營業執照編號一樣,我們就認定兩個商家是同一家,因此營業執照編號就可以看作標識符。而經緯度位置就是一個值對象,經緯度位置這個值對象包含“經度”和“緯度”兩個屬性,經緯度位置沒有標識符,而且經緯度位置的經度和緯度兩個屬性也不會被修改,如果商家搬家了,我們只要重新創建一個新的經緯度位置的對象,然后重新賦值商家的經度和緯度屬性就可以了。當然,我們也可以取消經緯度位置這個值對象屬性,直接改為經度、緯度兩個屬性,也就是商家實體類包含營業執照編號、名稱、經度、緯度、電話等屬性,但是把經度和緯度作為一個值對象更能夠體現它們的整體關系。實體類幫助我們跟蹤對象的變更,而值對象則幫助我們把多個相關屬性當作一個整體。
9.2.7 聚合與聚合根
一個系統中會有很多的實體類(包含值對象)?,這些實體類之間有的關系緊密,有的關系很弱,有的沒有關系。面向對象設計的一個重要原則就是“高內聚,低耦合”?,我們同樣希望有關系的實體類緊密協作,而關系很弱或者沒有關系的實體類可以很好地被隔離。因此,我們可以把關系緊密的實體類放到一個聚合(aggregate)中,每個聚合中有一個實體類作為聚合根(aggregate root)?,所有對聚合內實體類的訪問都通過聚合根進行,外部系統只能持有對聚合根的引用,聚合根不僅僅是實體類,還是所在聚合的管理者。
聚合并不是簡單地把實體類組合在一起,而要協調聚合內若干實體類的工作,讓它們按照統一的業務規則運行,從而實現實體類數據訪問的一致性,這樣我們就能夠實現聚合內的“高內聚”?;聚合之間的關系很弱,一個聚合只能引用另外一個聚合的聚合根,這樣我們就能夠實現聚合間的“低耦合”?。
聚合體現的是現實世界中整體和部分的關系,比如訂單與訂單明細。整體封裝了對部分的操作,部分與整體有相同的生命周期。部分不會單獨與外部系統交互,與外部系統的交互都由整體來負責。
聚合的設計是DDD中比較難的工作?