7.1什么是DDD
DDD是Eric Evans在2003年出版的《領域驅動設計:軟件核心復雜性應對之道》(Domain-Driven Design: Tackling Complexity in the Heart of Software)一書中提出的具有劃時代意義的重要概念,是指通過統一語言、業務抽象、領域劃分和領域建模等一系列手段來控制軟件復雜度的方法論。
DDD的革命性在于領域驅動設計是面向對象分析的方法論,它可以利用面向對象的特性(封裝、多態)有效地化解復雜性,而傳統J2EE或Spring+Hibernate等事務性編程模型只關心數據。這些數據對象除了簡單的setter/getter方法外,不包含任何業務邏輯,業務邏輯都是以過程式的代碼寫在Service中。這種方式極易上手,但隨著業務的發展,系統也很容易變得混亂復雜。
7.2初步體驗DDD
在介紹DDD之前,我喜歡用這個銀行轉賬的案例來做一個DDD和事務腳本(Transaction Script)的簡單對比。我們要實現一個銀行轉賬的功能,如果用傳統的事務腳本方式實現,業務邏輯通常會被寫在MoneyTransferService中,而Account僅僅是getters和setters的數據結構,也就是所謂的“貧血模式”。其代碼如下所示:
publicclassMoneyTransferServiceTransactionScriptImpl implementsMoneyTransferService{ privateAccountDaoaccountDao; privateBankingTransactionRepositorybankingTransactionRepository; ... @Override publicBankingTransactiontransfer( StringfromAccountId,StringtoAccountId,doubleamount){ AccountfromAccount=accountDao.findById(fromAccountId); AccounttoAccount=accountDao.findById(toAccountId); ... doublenewBalance=fromAccount.getBalance()-amount; switch(fromAccount.getOverdraftPolicy()){ caseNEVER: if(newBalance0)?{ ????????throw?new?DebitException("Insufficient?funds"); ??????} ??????break; ????case?ALLOWED: ??????if?(newBalance?-limit)?{ ????????throw?new?DebitException( ????????????"Overdraft?limit?(of?"?+?limit?+")?exceeded:?"?+?newBalance); ??????} ??????break; ????} ????fromAccount.setBalance(newBalance); ????toAccount.setBalance(toAccount.getBalance()?+?amount); ????BankingTransaction?moneyTransferTransaction?= ????????new?MoneyTranferTransaction(fromAccountId,toAccountId,amount); ????bankingTransactionRepository.addTransaction(moneyTransferTransaction); ????return?moneyTransferTransaction; ??}}
上述代碼有些讀者可能會比較眼熟,因為大部分系統都是這么寫的。評審完需求,工程師畫幾張UML圖完成設計,就開始像上面這樣寫業務代碼了,這樣寫基本不用太動腦筋,完全是過程式的代碼風格。
同樣的業務邏輯,接下來看使用領域建模是怎么做的。在使用DDD之后,Account實體除賬號屬性之外,還包含了行為和業務邏輯,比如debit()和credit()方法。
publicclassAccount{ privateStringid; privatedoublebalance; privateOverdraftPolicyoverdraftPolicy; ... publicdoublebalance(){returnbalance;} publicvoiddebit(doubleamount){ this.overdraftPolicy.preDebit(this,amount); this.balance=this.balance-amount; this.overdraftPolicy.postDebit(this,amount); } publicvoidcredit(doubleamount){ this.balance=this.balance+amount; }}
透支策略OverdraftPolicy也不僅僅是一個Enum了,而是被抽象成包含業務規則并采用策略模式的對象。
publicinterfaceOverdraftPolicy{ voidpreDebit(Accountaccount,doubleamount); voidpostDebit(Accountaccount,doubleamount);}publicclassNoOverdraftAllowedimplementsOverdraftPolicy{ publicvoidpreDebit(Accountaccount,doubleamount){ doublenewBalance=account.balance()-amount; if(newBalance0)?{ ??????throw?new?DebitException("Insufficient?funds"); ????} ??} ??public?void?postDebit(Account?account,?double?amount)?{ ??}}public?class?LimitedOverdraft?implements?OverdraftPolicy?{ ??private?double?limit; ??.?.?. ??public?void?preDebit(Account?account,?double?amount)?{ ????double?newBalance?=?account.balance()?-?amount; ????if?(newBalance?-limit)?{ ??????throw?new?DebitException( ??????????"Overdraft?limit?(of?"?+?limit?+?")?exceeded:?"+newBalance); ????} ??} ??public?void?postDebit(Account?account,?double?amount)?{ ??}}
而Domain Service只需要調用Domain Entity對象完成業務邏輯。
publicclassMoneyTransferServiceDomainModelImpl implementsMoneyTransferService{ privateAccountRepositoryaccountRepository; privateBankingTransactionRepositorybankingTransactionRepository; ... @Override publicBankingTransactiontransfer( StringfromAccountId,StringtoAccountId,doubleamount){ AccountfromAccount=accountRepository.findById(fromAccountId); AccounttoAccount=accountRepository.findById(toAccountId); ... fromAccount.debit(amount); toAccount.credit(amount); BankingTransactionmoneyTransferTransaction= newMoneyTranferTransaction(fromAccountId,toAccountId,amount); bankingTransactionRepository.addTransaction(moneyTransferTransaction); returnmoneyTransferTransaction; }}
通過DDD重構后,雖然類的數量比以前多了一些,但是每個類的職責更加單一,代碼的可讀性和可擴展性也隨之提高。
基于 Spring Cloud Alibaba + Gateway + Nacos + RocketMQ + Vue & Element 實現的后臺管理系統 + 用戶小程序,支持 RBAC 動態權限、多租戶、數據權限、工作流、三方登錄、支付、短信、商城等功能
項目地址:https://github.com/YunaiV/yudao-cloud
視頻教程:https://doc.iocoder.cn/video/
7.3數據驅動和領域驅動
7.3.1數據驅動
目前主流的開發模式是由數據驅動的。數據驅動的開發很容易上手,
有了業務需求,創建數據庫表,然后編寫業務邏輯,開發過程如圖7-1所示。數據驅動以數據庫為中心,其中最重要的設計是數據模型,但隨著業務的增長和項目的推進,軟件開發和維護的難度會急劇增加。
圖7-1數據驅動研發過程
以客戶關系管理(Customer Relationship Management,CRM)為例,其中很重要的概念有銷售、機會、客戶、私海、公海,實體的定義分別如下。
銷售(Sales):公司的銷售人員,一個銷售可以擁有多個銷售機會。
機會(Opportunity):銷售機會,每個機會包含至少一個客戶信息,且歸屬于一個銷售人員。
客戶(Customer):客戶,也就是銷售的對象。
私海(Private sea):專屬于某個銷售人員的領地(Territory),私海里面的客戶,其他銷售人員不能觸碰。
公海(Public sea):公共的領地,所有銷售人員都可以從公海里撿入客戶到其私海。
按照我們曾經學習的數據庫建模理論,對于上面的場景,不難畫出圖7-2所示的實體聯系(Entity Relationship,ER)圖。
圖7-2CRM的ER圖
可以看到,圖7-2所示的ER圖中不存在公海和私海,因為所謂的機會在私海,就是這個機會是不是歸屬某個銷售,這樣我們只需要看機會上是否有salesId。如果有,說明機會被某個銷售占有,也就是在私海中;反之,這個機會就在公海中。
在這種開發模式下,最后的產出是幾張數據庫表,以及針對表中數據進行操作的事務腳本,如圖7-3所示。
圖7-3事務腳本實現
7.3.2領域驅動
領域驅動設計關心的是業務中的領域劃分(戰略設計)和領域建模(戰術設計),其開發過程不再以數據模型為起點,而是以領域模型為出發點,研發過程如圖7-4所示。領域模型對應的是業務實體,在程序中主要表現為類、聚合根和值對象,它更加關注業務語義的顯性化表達,而不是數據的存儲和數據之間的關系。 這是“領域驅動設計”和“數據驅動設計”之間顯著的區別。
圖7-4領域驅動研發過程
仍以上面的CRM為例。假如我們先不考慮數據模型,而是采用面向對象分析(Object Oriented Analysis,OOA)對這個場景進行領域建模,那么可以得到圖7-5所示的領域模型。
圖7-5CRM的領域模型
可以看到,在圖7-5中,領域模型的描述更加貼近業務,一些重要的業務術語和概念沒有丟失,更完整地表達了業務語義。即使是產品經理或者業務人員,也不難看懂這樣的領域模型,甚至他們可以和技術人員一起參與到梳理領域模型和創建活動中來。
通過DDD的戰略設計和戰術設計,我們可以為問題域劃分出合適的子域,并對域中的業務進行建模。圖7-6所示是我們在實際工作中為CRM進行的領域戰略設計。
圖7-6CRM的領域劃分
7.3.3ORM
很明顯,領域模型和數據模型并不是一一對應的關系,但也不排除,有些情況領域模型和數據模型是趨同的,但是大部分情況都需要做一層映射(Mapping)。為了彌補二者之間的差異,行業先驅們做了很多關于映射工作的嘗試,這種技術有一個名稱叫作對象關系映射(Object Relationship Mapping,ORM),如圖7-7所示。
圖7-7對象關系映射
ORM曾經非常火,記得當年Hibernate才出現時,我用盡了其中的高級技巧,比如繼承關系映射、多對多關系映射……結果弄出來的東西卻變成了“四不像”,既不像Entity,也不像數據對象(Data Object,DO)。
ORM的問題在于它太理想化,期望通過工具把數據建模和領域建模合一,這樣的嘗試注定是很難成功的。仍以上述的CRM案例為例,在數據模型中根本就沒有私海和公海這兩個實體,工具是無法映射的。因此,Hibernate和JPA的衰落是可以預見的。現在使用最多的是MyBatis,它很簡單,完全不理會復雜的關系和對象之間的復雜關系映射,只做數據庫表和DO之間的簡單映射。
復雜的數據庫關系和對象關系之間的差異,其本質是數據模型和領域模型之間的差異,而這種差異的多樣性和靈活性是很難通過規則預先定義的,這也是為什么工具的作用會很有限。現在的互聯網大廠大多使用MyBatis,原因也在于此。因此,如果你打算實踐DDD,請一定不要讓工具幫你去建模,工具不會抽象,也不會思考,還是要老老實實自己動手去建。
審核編輯:劉清
-
CRM
+關注
關注
1文章
145瀏覽量
21111 -
數據驅動器
+關注
關注
0文章
5瀏覽量
6163 -
ddd
+關注
關注
0文章
23瀏覽量
2918
原文標題:DDD的精髓
文章出處:【微信號:芋道源碼,微信公眾號:芋道源碼】歡迎添加關注!文章轉載請注明出處。
發布評論請先 登錄
相關推薦
評論