曾經參與過系統維護或是在現有系統中進行迭代開發的軟件工程師們,大家是否有過這樣的痛苦經歷:當須要修改一個Bug的時候,面對一個類中成百上千行的代碼,沒有註釋,千奇百怪的方法和變量名字,層層嵌套的方法調用,混亂不堪的結構,不要說準確找到Bug所在的位置,就是要清晰知道一段代碼到底是作了什麼也很是困難,最終,改對了一個Bug,卻多冒出N個新Bug;一樣的狀況,當你拿到一份新的需求,須要在現有系統中添加功能的時候,面對一行行徹底過程式的代碼,須要使用一個功能時,不知道是應該本身編寫,仍是應該尋找是否已經存在的方法,編寫一個很是簡單的新、刪、改功能,卻要費盡九牛二虎之力,最終發現,系統存在着太多的重複邏輯,閱讀、測試、修改很是困難。在經歷了這些痛苦以後,大家是否會不約而同的發出一個感慨:與其進行系統維護和迭代開發,還不如從新設計開發一個新的系統來得痛快?html
面對這一系列讓軟件嵌入無底泥潭的問題,基於面向對象思想的領域驅動設計方法是一個很好的解決方法。從事過系統設計的富有經驗的設計師們,對職責單一原則、信息專家、充血/貧血模型、模型驅動設計這些名詞或概念應該不會感到陌生。面向對象的設計大師Martin Fowler不止一次的在他的Blog和著做《企業應用架構模式》中倡導過上述概論在設計中的巨大威力,而另一位領域模型的出色專家Eric Evans的著做《領域驅動設計》也爲咱們提供了很多寶貴的經驗和方法。程序員
筆者從事系統設計多年,將會在本系列文章中把本人對領域驅動設計的理解,結合工做過程當中積累的實際項目經驗進行淺析,但願與你們交流學習。架構
在本系列博文的開篇中,我將會拿出一個顯示的例子,先用傳統的面向過程方式,使用貧血模型進行設計,而後再逐步加入需求變動,讓讀者發現,隨着系統的不斷變動,基於貧血模型的設計將會讓系統慢慢陷入泥潭,愈來愈難於維護,而後再用基於面向對象的領域驅動設計從新上述過程,經過對比展現領域驅動設計對於複雜的業務系統的威力。運維
假設如今有一個銀行支付系統項目,其中的一個重要的業務用例是帳戶轉帳業務。系統使用迭代的方式進行開發,在1.0版本中,該用例的功能需求很是簡單,事件流描述以下:分佈式
主事件流:模塊化
1) 用戶登陸銀行的在線支付系統學習
2) 選擇用戶在該銀行註冊的網上銀行帳戶測試
3) 選擇須要轉帳的目標帳戶,輸入轉帳金額,申請轉帳spa
4) 銀行系統檢查轉出帳戶的金額是否足夠設計
5) 從轉出帳戶中扣除轉出金額(debit),更新轉出帳戶的餘額
6) 把轉出金額加入到轉入帳戶中(credit),更新轉入帳戶的餘額
備選事件流:
4a)若是轉出帳戶中的餘額不足,轉帳失敗,返回錯誤信息
面向過程的設計方式(貧血模型)
設計方案以下(忽略展現層部分):
1) 設計一個帳戶交易服務接口AccountingService,設計一個服務方法transfer(),並提供一個具體實現類AccountingServiceImpl,全部帳戶交易業務的業務邏輯都置於該服務類中。
2) 提供一個AccountInfo和一個Account,前者是一個用於與展現層交換帳戶數據的帳戶數據傳輸對象,後者是一個帳戶實體(至關於一個EntityBean),這兩個對象都是普通的JavaBean,具備相關屬性和簡單的get/set方法。
下面是AccountingServiceImpl.transfer()方法的實現邏輯(僞代碼):
能夠看到,因爲1.0版本的功能需求很是簡單,按面向過程的設計方式,把全部業務代碼置於AccountingServiceImpl中徹底沒有問題。
這時候,新需求來了,在1.0.1版本中,須要爲帳戶轉帳業務增長以下功能,在轉帳時,首先須要判斷帳戶是否可用,而後,帳戶的餘額還要分紅兩部分:凍結部分和活躍部分,處於凍結部分的金額不能用於任何交易業務,咱們來看看變動後的代碼:
能夠看到,狀況變得稍微複雜了,這時候,1.0.2的需求又來了,須要在每次交易成功後,建立一個交易明細帳,因而,咱們又必須在transfer()方面裏面增長建立並持久化交易明細帳的業務邏輯:
業務需求不斷複雜化:帳戶每筆轉帳的最大額度須要由其信用指數肯定、須要根據銀行的手續費策略計算並扣除必定的手續費用……,隨着業務的複雜化,transfer()方法的邏輯變得愈來愈複雜,逐漸造成了上文所述的成百上千行代碼。有經驗的程序員可能會作出類此「方法抽取」的重構,把轉帳業務按邏輯劃分紅若干塊:判斷餘額是否足夠、判斷帳戶的信用指數以肯定每筆最大轉帳金額、根據銀行的手續費策略計算手續費、記錄交易明細帳……,從而使代碼更加結構化。這是一個好的開始,但仍是顯然不足。
假設某一天,系統需求增長一個新的模塊,爲系統增長一個網上商城,讓銀行用戶能夠進行在線購物,而在線購物也存在着不少與帳戶貸記借記業務相同或類似的業務邏輯:判斷餘額是否足夠、對帳戶進行借貸操做(credit/debit)以改變餘額、收取手續費用、產生交易明細帳……
面對這種狀況,有兩種解決辦法:
1) 把AccountingServiceImpl中的相同邏輯拷貝到OnlineShoppingServiceImplementation中
2) 讓OnlineShoppingServiceImpl調用AccountingServiceImpl的相同服務
顯然,第二種方法比第一種方法更好,結構更清晰,維護更容易。但問題在於,這樣就會造成網上商城服務模塊與帳戶收支服務模塊的沒必要要的依賴關係,系統的耦合度高了,若是系統爲了更靈活的伸縮性,讓每一個大業務模塊獨立進行部署,還須要由於二者的依賴關係創建分佈式調用,這無疑增長了設計、開發和運維的成本。
有經驗的設計人員可能會發現第三種解決辦法:把相同的業務邏輯抽取成一個新的服務,做爲公共服務同時供上述兩個業務模塊使用。這只是筆者將會立刻討論的方案——使用領域驅動設計。
面向過程的領域驅動設計方式(充血模型)
爲了節省篇幅,這裏就直接以最複雜的業務需求來進行設計。
領域驅動設計的一個重要的概念是領域模型,首先,咱們根據業務領域抽象出如下核心業務對象模型:
Account:帳戶,是整個系統的最核心的業務對象,它包括如下屬性:對象標識、帳戶號、是否有效標識、餘額、凍結金額、帳戶交易明細集合、帳戶信用等級。
AccountTransactionDetails:帳戶交易明細,它從屬於帳戶,每一個帳戶有多個交易明細,它包括如下屬性:對象標識、所屬帳戶、交易類型、交易發生金額、交易發生時間。
AccountCreditDegree:帳戶信用等級,它用於限制帳戶的每筆交易發生金額,包含如下屬性:對象標識、對應帳戶、信用指數。
BankTransactionFeeCalculator:銀行交易手續費用計算器,它包含一個常量:每筆交易的手續費上限。
咱們知道,領域對象除了具備自身的屬性和狀態以外,它的一個很重要的標誌是,它具備屬於本身職責範圍以內的行爲,這些行爲封裝了其領域內的領域業務邏輯。因而,咱們進行進一步的建模,根據業務需求爲領域對象設計業務方法:
根據職責單一的原則,咱們把功能需求中描述的功能合理的分配到不一樣的領域對象中:
Account:
(咱們能夠看到,後兩個業務方法被聲明爲protected,具體緣由見後述)
AccountCreditDegree:
BankTransactionFeeCalculator:
通過這樣的設計,前例中全部放置在服務對象的業務邏輯被分別劃入不一樣的負責相關職責的領域對象當中,下面的時序圖描述了AccountingServiceImpl的轉帳業務的實現邏輯(爲了簡化邏輯,咱們忽略掉事物、持久化等邏輯):
再看看AccountingServiceImpl.transfer()的實現邏輯:
咱們能夠看到,上例那些複雜的業務邏輯:判斷餘額是否足夠、判斷帳戶是否可用、改變帳戶餘額、計算手續費、判斷交易額度、產生交易明細帳……,都再也不存在於AccountingServiceImplementation的transfer方法中,它們被委派給負責這些業務的領域對象的業務方法中去,如今應該猜到爲何Account中有兩個方法被聲明爲protected了吧,由於他們是在debit和credit方法被調用時,由這兩個方法調用的,對於AccountingServiceImpl來講,因爲產生交易明細(createTransactionDetails)和更新帳戶信用指數(updateCreditIndex)都不屬於其職責範圍,它不須要也無權使用這些邏輯。
咱們能夠看到,使用領域驅動設計至少會帶來下述優勢:
再看看若是這時須要加入網上商城的一個新的模塊,開發人員須要怎麼去作,還記得上面提過的第三種方案嗎?就是把帳戶貸記和借記的相關業務抽取到成一個公共服務,同時供銀行在線支付系統和網上商城系統服務,其實這個公共的服務,本質上就是這些具備領域邏輯的領域對象:Account、AccountCreditDegree……,由此咱們又能夠發現領域驅動設計的一大優勢:
筆者經驗尚淺,並且文筆拙劣,但願經過這樣的一個場景的分析比較,能讓讀者初步認識到基於面向對象的領域驅動設計的威力,並在實際項目中嘗試應用。本篇是領取驅動設計系列博文的第一篇,在系列文章的第二篇博文中,筆者將會淺析VO、DTO、DO、PO的概念、用處和區別,敬請各位對本系列博文感興趣的讀者關注並給予指導修正。