|
2004年,當Eric Evans的那本《領域驅動設計——軟件核心復雜性應對之道》(后文簡稱《領域驅動設計》)出版時,我還在念高中,接觸到領域驅動設計(DDD)已經是8年后的事情了。那時,我正打算在軟件開發之路上更進一步,經同事介紹,我開始接觸DDD。
我想,多數有經驗的程序開發者都應該聽說過DDD,并且嘗試過將其應用在自己的項目中。不知你是否遇到過這樣的場景:你創建了一個資源庫(Repository),但一段時間之后發現這個資源庫和傳統的DAO越來越像了,你開始反思自己的實現方式是正確的嗎?或者,你創建了一個聚合,然后發現這個聚合是如此的龐大,它為什么引用了如此多的對象,難道又是我做錯了嗎?
其實你并不孤單,我相信多數同仁都曾遇到過相似的問題。前不久,我一個同事給我展示了他在2007年買的那本已經被他韋編三絕過的《領域驅動設計》,他告訴我,讀過好幾遍后,他依然不知道如何將DDD付諸實踐。Eric那本書固然是好,無可否認,但是我們程序員總希望看到一些實際的例子能夠切實將DDD落地以指導我們的日常開發。
于是,在Eric的書出版將近10年之后,我們有了《實現領域驅動設計》,作為該書的譯者,我有幸通讀了本書,受益匪淺,得到的結論是:好的軟件就應該是DDD的。
就像在微電子領域有知識產權核(Intellectual Property)一樣,DDD將一個軟件系統的核心業務功能集中在一個核心域里面,其中包含了實體、值對象、領域服務、資源庫和聚合等概念。在此基礎上,DDD提出了一套完整的支撐這樣的核心領域的基礎設施。此時,DDD已經不再是“面向對象進階”那么簡單了,而是演變成了一個系統工程。
所謂領域,即是一個組織的業務開展方式,業務價值便體現在其中。長久以來,我們程序員都是很好的技術型思考者,我們總是擅長從技術的角度來解決項目問題。但是,一個軟件系統是否真正可用是通過它所提供的業務價值體現出來的。因此,與其每天鉆在那些永遠也學不完的技術中,何不將我們的關注點向軟件系統所提供的業務價值方向思考思考,這也正是DDD所試圖解決的問題。
在DDD中,代碼就是設計本身,你不再需要那些繁文縟節的并且永遠也無法得到實時更新的設計文檔。編碼者與領域專家再也不需要翻譯才能理解對方所表達的意思。
DDD有戰略設計和戰術設計之分。戰略設計主要從高層“俯視”我們的軟件系統,幫助我們精準地劃分領域以及處理各個領域之間的關系;而戰術設計則從技術實現的層面教會我們如何具體地實施DDD。
DDD之戰略設計
需要指出的是,DDD絕非一套單純的技術工具集,但是我所看到的很多程序員卻的確是這么認為的,并且也是懷揣著這樣的想法來使用DDD的。過于拘泥于技術上的實現將導致DDD-Lite。簡單來講,DDD-Lite將導致劣質的領域對象,因為我們忽略了DDD戰略建模所帶來的好處。
DDD的戰略設計主要包括領域/子域、通用語言、限界上下文和架構風格等概念。
領域和子域(Domain/Subdomain)
既然是領域驅動設計,那么我們主要的關注點理所當然應該放在如何設計領域模型上,以及對領域模型的劃分。
領域并不是多么高深的概念,比如,一個保險公司的領域中包含了保險單、理賠和再保險等概念;一個電商網站的領域包含了產品名錄、訂單、發票、庫存和物流的概念。這里,我主要講講對領域的劃分,即將一個大的領域劃分成若干個子域。
在日常開發中,我們通常會將一個大型的軟件系統拆分成若干個子系統。這種
劃分有可能是基于架構方面的考慮,也有可能是基于基礎設施的。但是在DDD中,我們對系統的劃分是基于領域的,也即是基于業務的。
于是,問題也來了:首先,哪些概念應該建模在哪些子系統里面?我們可能會發現一個領域概念建模在子系統A中是可以的,而建模在子系統B中似乎也合乎情理。第二個問題是,各個子系統之間的應該如何集成?有人可能會說,這不簡單得就像客戶端調用服務端那么簡單嗎?問題在于,兩個系統之間的集成涉及到基礎設施和不同領域概念在兩個系統之間的翻譯,稍不注意,這些概念就會對我們精心創建好的領域模型造成污染。
如何解決?答案是:限界上下文和上下文映射圖。
限界上下文(Bounded Context)
在一個領域/子域中,我們會創建一個概念上的領域邊界,在這個邊界中,任何領域對象都只表示特定于該邊界內部的確切含義。這樣邊界便稱為限界上下文。限界上下文和領域具有一對一的關系。
舉個例子,同樣是一本書,在出版階段和出售階段所表達的概念是不同的,出版階段我們主要關注的是出版日期,字數,出版社和印刷廠等概念,而在出售階段我們則主要關心價格,物流和發票等概念。我們應該怎么辦呢,將所有這些概念放在單個Book對象中嗎?這不是DDD的做法,DDD有限界上下文將這兩個不同的概念區分開來。
從物理上講,一個限界上下文最終可以是一個DLL(.NET)文件或者JAR(Java)文件,甚至可以是一個命名空間(比如Java的package)中的所有對象。但是,技術本身并不應該用來界分限界上下文。
將一個限界上下文中的所有概念,包括名詞、動詞和形容詞全部集中在一起,我們便為該限界上下文創建了一套通用語言。通用語言是一個團隊所有成員交流時所使用的語言,業務分析人員、編碼人員和測試人員都應該直接通過通用語言進行交流。
對于上文中提到的各個子域之間的集成問題,其實也是限界上下文之間的集成問題。在集成時,我們主要關心的是領域模型和集成手段之間的關系。比如需要與一個REST資源集成,你需要提供基礎設施(比如Spring 中的RestTemplate),但是這些設施并不是你核心領域模型的一部分,你應該怎么辦呢?答案是防腐層,該層負責與外部服務提供方打交道,還負責將外部概念翻譯成自己的核心領域能夠理解的概念。當然,防腐層只是限界上下文之間眾多集成方式的一種,另外還有共享內核、開放主機服務等,具體細節請參考《實現領域驅動設計》原書。限界上下文之間的集成關系也可以理解為是領域概念在不同上下文之間的映射關系,因此,限界上下文之間的集成也稱為上下文映射圖。
架構風格(Architecture)
DDD并不要求采用特定的架構風格,因為它是對架構中立的。你可以采用傳統的三層式架構,也可以采用REST架構和事件驅動架構等。但是在《實現領域驅動設計》中,作者比較推崇事件驅動架構和六邊形(Hexagonal)架構。
當下,面向接口編程和依賴注入原則已經在顛覆著傳統的分層架構,如果再進一步,我們便得到了六邊形架構,也稱為端口和適配器(Ports and Adapters)。在六邊形架構中,已經不存在分層的概念,所有組件都是平等的。這主要得益于軟件抽象的好處,即各個組件的之間的交互完全通過接口完成,而不是具體的實現細節。正如Robert C. Martin所說:
抽象不應該依賴于細節,細節應該依賴于抽象。
采用六邊形架構的系統中存在著很多端口和適配器的組合。端口表示的是一個軟件系統的輸入和輸出,而適配器則是對每一個端口的訪問方式。比如,在一個Web應用程序中,HTTP協議可以作為一個端口,它向用戶提供HTML頁面并且接受用戶的表單提交;而Servlet(對于Java而言)或者Spring中的Controller則是相對應于HTTP協議的適配器。再比如,要對數據進行持久化,此時的數據庫系統則可看成是一個端口,而訪問數據庫的Driver則是相應于數據庫的適配器。如果要為系統增加新的訪問方式,你只需要為該訪問方式添加一個相應的端口和適配器即可。
那么,我們的領域模型又如何與端口和適配器進行交互呢?
上文已經提到,軟件系統的真正價值在于提供業務功能,我們會將所有的業務功能分解為若干個業務用例,每一次業務用例都表示對軟件系統的一次原子操作。所以首先,軟件系統中應該存在這樣的組件,他們的作用即以業務用例為單位向外界暴露該系統的業務功能。在DDD中,這樣的組件稱為應用層(Application Layer)。
在有了應用層之后,軟件系統和外界的交互便變成了適配器和應用層之間的交互,如圖-1所示。
圖-1 六邊形架構
從圖-1中可以看出,領域模型位于應用程序的核心部分,外界與領域模型的交互都通過應用層完成,應用層是領域模型的直接客戶。然而,應用層中不應該包含有業務邏輯,否則就造成了領域邏輯的泄漏,而應該是很薄的一層,主要起到協調的作用,它所做的只是將業務操作代理給我們的領域模型。同時,如果我們的業務操作有事務需求,那么對于事務的管理應該放在應用層上,因為事務也是以業務用例為單位的。
應用層雖然很薄,但卻非常重要,因為軟件系統的領域邏輯都是通過它暴露出去的,此時的應用層扮演了系統門面(Facade)的角色。
DDD之戰術設計
戰略設計為我們提供一種高層視野來審視我們的軟件系統,而戰術設計則將戰略設計進行具體化和細節化,它主要關注的是技術層面的實施,也是對我們程序員來得最實在的地方。
行為飽滿的領域對象
我們希望領域對象能夠準確地表達出業務意圖,但是多數時候,我們所看到的卻是充滿getter和setter的領域對象,此時的領域對象已經不是領域對象了,而是Martin Fowler所稱之為的貧血對象。
放到Java世界中,多年以來,Java Bean規范都引誘著程序員們以“自然而然又合乎情理”的方式創建著無數的貧血對象,而一些框架也規定對象必須提供getter和setter方法,比如Hibernate的早期版本。那么,貧血對象到底有什么壞處呢?來看一個例子:要修改一個客戶(Customer)的郵箱地址,在使用setter方法時為:
public class Customer { private String email; public void setEmail(String email) { this.email = email; }}
it知識庫:領域驅動設計實現之路,轉載需保留來源!
鄭重聲明:本文版權歸原作者所有,轉載文章僅為傳播更多信息之目的,如作者信息標記有誤,請第一時間聯系我們修改或刪除,多謝。