本文首發於 vivo互聯網技術 微信公衆號
連接: https://mp.weixin.qq.com/s/gk-Hb84Dt7JqBRVkMqM7Eg
做者:張文博前端
領域驅動設計(Domain Driven Design,DDD)其實並不是新理論,你們能夠看看 Eric Evans 編著的《領域驅動設計》原稿首版是2003年,距今已十餘年時間。與如今的分佈式、微服務相比,絕對是即將步入中年的「老傢伙」了。java
直到近些年微服務理論被提出、被互聯網行業普遍使用,人們彷佛又從新發現了領域驅動設計的價值。因此看起來也確實是由於微服務,領域驅動設計才迎來了第二春。mysql
不過我發現你們對DDD也存有一些誤區,使其漸漸成了一門「高深的玄學」,隨之又被你們束之高閣。我本人在過去兩年多的時間裏,研讀過多本DDD相關的經典論著、也請教過一些資深DDDer,並在項目中實踐過。程序員
不過在初步學習、實踐以後我又帶着疑問與本身的思考從新讀了一遍相關的著述理論。逐漸領悟到DDD做爲一種思想,其實離咱們很近。面試
我把本身的學習過程、思考編寫成系列文章,與你們一塊兒探討學習,但願你們可以有所收穫,固然其中不正確的地方也歡迎你們批評指正。redis
同時,在文章中我也會引用相關的論著或者一些我認爲不錯的案例素材,權當是咱們對這些知識的詳細詮釋,在這裏一併對這些DDD前輩的不倦探索表示感謝。spring
(DDD相關的經典論著)sql
DDD是什麼?衆裏尋她千百度,驀然回首,「DDD是一種能夠借鑑的思想,而非嚴格遵循的方法論」。數據庫
當咱們面向業務開發的過程當中,應該首先思考領域模型而不是如何建表。安全
我聽過太多業務開發的聲音,「面試造航母、工做擰螺絲」,平常工做就是建表寫增刪改查。爲何會有這樣的認知,其根源在於表驅動設計思想而非領域驅動設計。
前者只能增長數據庫的表數量,然後者纔會造成長期的、具備業務意義的模型,這樣的系統生命力才更加長久。咱們也才能用工程的方法來編碼,從編碼轉身爲業務域的開發專家。
有不少關於領域驅動設計的論述中都並未明確咱們如何獲得「領域」,只有合理的領域模型纔能有效驅動設計開發。因此建好領域模型是關鍵,對於領域模型的思考與技術框架升級一樣重要。我曾經在互聯網部門分享過如何進行領域建模,也歡迎你們與我交流溝通,有興趣的讀者也能夠重點閱讀一下《UML和模式應用》相關章節。
在討論DDD以前咱們先來討論一下「解耦」,這個詞是咱們在平常編碼時候常常說起的詞語。一個具備工匠精神的程序員必定會在代碼審查階段對一些巨無霸函數或者類進行拆分,使各部分的功能更加聚焦、下降耦合。
另外一方面,在架構方面咱們也會重視「解耦」,由於一個模塊之間隨意耦合的系統將是全部人的噩夢之源。所以,除了整潔的代碼咱們還須要關注整潔的架構。
架構的三要素:職責明確的模塊或者組件、組件間明確的關聯關係、約束和指導原則。內聚的組件必定有明確的邊界,而這個明確的邊界必然做爲相關的約束指導從此的發展。
分層架構是運用最爲普遍的架構模式,幾乎每一個軟件系統都須要經過層來隔離不一樣的關注點,以此應對不一樣需求的變化,使得這種變化能夠獨立進行;各個層、甚至同一層中的各個組件都會以不一樣速率發生變化。
這裏所謂的「以不一樣速率發生變化」,其實就是引發變化的緣由各有不一樣,這正好是單一職責原則(Single-Responsibility Principle,SRP)的體現。即「一個類應該只有一個引發它變化的緣由」,換言之,若是有兩個引發類變化的緣由,就須要分離。
單一職責原則能夠理解爲架構原則,這時要考慮的就不是類,而是層次。例如網絡七層協議是一個定義的很是好的、經典的分層架構,簡單、易於學習理解,最終被普遍使用進而大大推進了網絡通訊的發展。
一般狀況下,咱們會把軟件系統分爲這幾個層:UI界面(或者接入層)、應用獨有的業務邏輯、領域普適的業務邏輯、數據庫等。
接下來,還有什麼不一樣緣由的變動呢?答案正是這些業務邏輯自己!在每一層內部,不一樣的業務場景發生變化的緣由、頻次也都不一樣,不一樣的場景咱們分別定義爲業務用例。由此,咱們能夠總結出一個模式:在將系統水平切分紅多個分層的同時,按用例將其切分紅多個垂直切片。這樣作的好處就是對單個用例的修改並不會影響其餘用例。
若是咱們同時對支持這些用例的UI和數據庫也進行了分組,那麼每一個用例使用各自的UI表現與數據庫,這樣就作到了自上而下的解耦。另外一方面,有層次就有依賴。在OSI協議中,上層透明的依賴下層。可是在軟件架構中,咱們更強調「依賴抽象」。即組件A依賴B的功能,咱們的作法是在A中定義其須要用到的接口,由B去實現對應接口能力,這樣就作到了可插拔,未來咱們能夠把B替換爲一樣實現了接口能力的組件C而對系統不會形成影響。
分層架構中給人的感受是每一層都一樣重要,但若是咱們把關注的重點放在領域層,同時把依賴關係按照業務由重到輕造成一個以領域層爲中心的環,即演變爲一種整潔的架構風格。這裏不是說其餘層不重要,僅僅是爲了凸顯承載了業務核心的領域能力。
整潔架構最主要原則是依賴原則,它定義了各層的依賴關係,越往裏,依賴越低,代碼級別越高。外圓代碼依賴只能指向內圓,內圓不知道外圓的任何事情。通常來講,外圓的聲明(包括方法、類、變量)不能被內圓引用。一樣的,外圓使用的數據格式也不能被內圓使用。
整潔架構各層主要職能以下:
Entities:實現領域內核心業務邏輯,它封裝了企業級的業務規則。一個 Entity 能夠是一個帶方法的對象,也能夠是一個數據結構和方法集合。通常咱們建議建立充血模型。
Use Cases:實現與用戶操做相關的服務組合與編排,它包含了應用特有的業務規則,封裝和實現了系統的全部用例。
Interface Adapters:它把適用於 Use Cases 和 entities 的數據轉換爲適用於外部服務的格式,或把外部的數據格式轉換爲適用於 Use Casess 和 entities 的格式。
咱們把整潔架構的外部依賴按照其輸入輸出功能、資源類型進行整合。將存儲、中間件、與其餘系統的集成、http調用分別暴露一個端口。則會演變成下面的架構圖。
「Allow an application to equally be driven by users, programs, automated test or batch scripts, and to be developed and tested in isolation from its eventual run-time devices and databases.」「系統能平等地被用戶、其餘程序、自動化測試或腳本驅動,也能夠獨立於其最終的運行時設備和數據庫進行開發和測試」這是六邊形的精髓。
該架構由端口和適配器組成,所謂端口是應用的入口和出口,在許多語言中,它以接口的形式存在。例如以取消訂單爲例,「發送訂單取消通知」能夠被認爲是一個出口端口,訂單取消的業務邏輯決定了什麼時候調用該端口,訂單信息決定了端口的輸入,而端口爲上游的訂單相關業務屏蔽了其實現細節。
而適配器分爲兩種,主適配器(別名Driving Adapter)表明用戶如何使用應用,從技術上來講,它們接收用戶輸入,調用端口並返回輸出。Rest API是目前最多見的應用使用方式,以取消訂單爲例,該適配器實現Rest API的Endpoint,並調用入口端口OrderService,固然service內部可能發送OrderCancelled事件。同一個端口可能被多種適配器調用,本場景的取消訂單也可能會被實現消息協議的Driving Adapter調用以便異步取消訂單。
次適配器(別名Driven Adapter)實現應用的出口端口,向外部工具執行操做,例如向MySQL執行SQL,存儲訂單;使用Elasticsearch的API搜索產品;使用郵件/短信發送訂單取消通知。有別於傳統的分層形象,造成一個六邊形,所以也會稱做六邊形架構。
我愚昧的認爲,DDD即業務+解耦。大道至簡、多麼熟悉的場景,由於這就是咱們在作的事情,只不過咱們可能過於關注使用了什麼技術框架、用了哪些中間件、寫了哪些通用的class。
實際上DDD如同辯證惟物主義思想同樣,哪怕咱們在軟件項目的某一個環節用到了,只要這個思想爲咱們解決了實際問題就夠了。咱們沒有必要爲了DDD而去DDD,咱們必定是從問題中來再回到問題中去。
藉助DDD能夠改變開發者對業務領域的思考方式,要求開發者花費大量的時間和精力來仔細思考業務領域,研究概念和術語,而且和領域專家交流以發現,捕捉和改進通用語言,甚至發現模型乃至系統架構層面的不合理之處。固然有可能你的團隊中並無相關業務的專家,那麼此時你本身必須成爲業務專家。
一般來講咱們能夠將DDD的業務價值總結爲如下幾點:
你得到了一個很是有用的領域模型;
你的業務獲得了更準確的定義和理解;
領域專家能夠爲軟件設計作出貢獻;
更好的用戶體驗;
清晰的模型邊界;
更好的企業架構;
敏捷、迭代式和持續建模;
經過前面的論述,你腦海裏面必定閃爍幾個詞語「領域模型」「解耦」「依賴抽象」「邊界」。這些通用的分析方法必定是放之四海而皆有效的。因此我認爲當你按照這幾個原則進行思考的時候就已經在DDD的路上向前邁進了一步,接下來咱們結合界限上下文、Repository這兩個最容易被你們所忽略的地方來進一步闡述。
在這些步驟都作完之後,你再決定接下來如何去編碼開發。不過我敢確定,你在這個過程當中已經獲得了不少高業務價值的東西。
接下來如何去實現,你能夠根據實際狀況。我以爲戰略DDD比戰術DDD更重要,我想這就是DDD做爲一種思想的神奇所在。如同金庸筆下的少林絕學易筋經同樣,一套並沒有明確招式的內功心法卻能打遍武林。
領域中還同時存在問題空間(problem space)和解決方案空間(solution space)。在問題空間中,咱們思考的是業務所面臨的挑戰,而在解決方案空間中,咱們思考如何實現軟件以解決這些業務挑戰。
問題空間是領域的一部分,對問題空間的開發將產生一個新的核心域。對問題空間的評估應該同時考慮已有子域和額外所需子域。所以,問題空間是核心域和其餘子域的組合。問題空間中的子域一般隨着項目的不一樣而不一樣,他們各自關注於當前的業務問題,這使得子域對於問題空間的評估很是有用。子域容許咱們快速地瀏覽領域中的各個方面,這些方面對於解決特定的問題是必要的。
一般,咱們但願將子域一對一地對應到限界上下文。這種作法顯式地將領域模型分離到不一樣的業務板塊中,並將問題空間和解決方案空間融合在一塊兒。
可是在實踐中,這種作法並不老是可能的,想像一下,誰沒有維護過「毛線團」系統,如今咱們就要藉助界限上下文來安全的、合理的、快速的理順這堆交織不清的關係。
不少書籍或者文章講解DDD,老是說突出應該怎麼構建代碼包結構,使用什麼技術框架。我認爲這是不徹底適用的,因此我會花較多時間來闡述一下如何藉助界限上下文來理順這堆「毛線團」。
我直接使用了《實現領域驅動設計》的相關章節的配圖,權當是我對這個圖的註釋吧。
遺留的電子商務系統是個典型的「大線團」,咱們按照經驗將其在邏輯上拆解爲:產品目錄子域、訂單子域、fa票子域,固然你也能夠拆解出更多的子域,甚至將產品目錄子域繼續向下分解爲類目子域、商品子域(虛線是邏輯子域)。另外還有一個專門用於庫存管理的庫存系統、以及用於銷售預測的預測系統。
因爲歷史緣由電商系統裏面也存在物流相關的業務邏輯,同時物流又不可避免的做用於庫存邏輯之上。而每每最難以把握的就是這部分相交的地方,這纔是實際的項目場景,咱們一般作法是將其歸併爲一個新的履約系統,做爲一個支撐子域去輔助主要的電商系統。
固然,隨着業務不斷髮展,咱們的履約模式(好比支持同城當日達、商家倉儲發貨、電商集貨倉發貨、退貨等等)、庫存類型(調撥庫存、越庫操做、臨期庫存、殘次庫存等等)愈來愈複雜,咱們考慮將其再向下分解爲履約系統2.0、庫存系統2.0。
核心就是咱們能夠在概念上使用多個子域來分解較大的界限上下文,也能夠將多個分散的界限上下文包含在同一個新的子域當中,最終作到「子域和界限上下文一一對應」。我我的以爲,這個過程是最考驗內功心法的地方。
上面咱們已經說了會拆解出來新的子域,目的使「整潔乾淨」的界限上下文可以一對一的解決這個子域對應的問題空間,可是隨着拆解就必然致使「關聯關係」。由於要解決問題空間,必須使用對應的子域,你能夠把它拆解出去,可是它始終存在於依賴網中。
咱們通用的作法是在相交的地方,定義接口。由支撐的界限上下文去實現,能夠作到支撐上下文的插拔式切換。這裏仍然是咱們強調的「依賴抽象」「解耦」。
「對於每種須要進行全局訪問的對象,咱們都應該建立另外一個對象來做爲這些對象的提供方,就像是在內存中訪問這些對象的集合同樣。爲這些對象建立一個全局接口以供客戶端訪問。爲這些對象建立添加和刪除方法……
此外,咱們還應該提供可以按照某種指定條件來查詢這些對象的方法……只爲聚合建立資源庫」引用自《領域驅動設計》。你們和個人疑問同樣,Repository是什麼?DAO與Repository什麼區別?爲何須要Repository?
首先,Repository 是一個獨立的層,介於領域層與數據映射層(數據訪問層)之間。
它的存在讓領域層感受不到數據訪問層的存在,它提供一個相似集合的接口提供給領域層進行領域對象的訪問。Repository 是倉庫管理員,領域層須要什麼東西只需告訴倉庫管理員,由倉庫管理員把東西拿給它,並不須要知道東西實際放在哪。其核心仍是「解耦」,因此咱們應該明確領域層只應該使用Repository獲取對象。
接下來,看看DAO與Repository什麼區別。
個人理解是這樣,你能夠將Repository看成 DAO 來看待,可是請注意一點,在設計Repository時,咱們應該採用面向集合的方式,而不是面向數據訪問的方式。這有助於你將本身的領域看成模型來看待,而不是 CRUD 操做;Repository是面向領域的,Repository定義的目的不是DB驅動的,Repository管理的數據的最小粒度是聚合根,這兩點和DAO有很大不一樣。
一般咱們建議把Repository定義爲一個集合而且只提供相似集合的接口,好比Add,Remove,Get這種操做。一言以蔽之,咱們要用集合的思想來操做聚合根,而不是傳統的面向DB的CRUD方法。
最**後來看看爲何須要Repository,我理解仍是「解耦」。**當咱們把Repository想象成一個資源庫,也不關心背後的持久化,這些也不是DDD該思考的東西,咱們能夠用mysql來實現,也能夠用mongo,甚至redis。尤爲是當咱們在更換底層存儲時候,領域層以及相關的服務並沒有任何影響。
如下是代碼示例:
package zwb.ddd.repository.sample.domain; import zwb.ddd.repository.sample.domain.model.BaseAggregateRoot; import java.util.List; /** * BaseAggregateRoot領域模型的基類,BaseSpecification適用於較爲複雜的查詢場景。 * @author wenbo.zhang * @date 2019-11-20 */ public interface IRepository<T extends BaseAggregateRoot, Q extends BaseSpecification> { T ofId(String id); void add(T t); void remove(String id); List<T> querySpecification(Q q); }
實現類:
package zwb.ddd.repository.sample.infrastructure; import org.springframework.stereotype.Component; import org.springframework.util.CollectionUtils; import zwb.ddd.repository.sample.domain.IRepository; import zwb.ddd.repository.sample.domain.BaseSpecification; import zwb.ddd.repository.sample.domain.model.BaseAggregateRoot; import zwb.ddd.repository.sample.domain.model.Customer; import zwb.ddd.repository.sample.domain.model.CustomerSpecification; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; /** * @author wenbo.zhang * @date 2019-11-20 */ @Component public class CustomerRepository implements IRepository { /** * Repository其具體實現上層是無感知的,若是之後咱們要切換爲redis、mysql只須要修改這一層便可。 */ Map<String, Customer> customerMap = new ConcurrentHashMap<>(); @Override public Customer ofId(String id) { return customerMap.get(id); } @Override public void add(BaseAggregateRoot aggregateRoot) { if (!(aggregateRoot instanceof Customer)) { return; } Customer customer = (Customer) aggregateRoot; customerMap.put(customer.getId(), customer); } @Override public void remove(String id) { customerMap.remove(id); } /** * 咱們在Specification裏面定義更加複雜的查詢條件 * * @param specification 此處舉例:基於id批量查詢 * @return */ @Override public List<Customer> querySpecification(BaseSpecification specification) { List<Customer> customers = new ArrayList<>(); if (!(specification instanceof CustomerSpecification)) { return customers; } if (CollectionUtils.isEmpty(specification.getIds())) { return customers; } specification.getIds().forEach(id -> { if (ofId(id) != null) { customers.add(ofId(id)); } }); return customers; } }
在平常項目中咱們使用mybatis,因此在Repository中會使用mybatis的DAO來進行操做,下圖是一個涉及到訂購的複雜場景。
咱們舉一個加盟業務來描述一下界限上下文的劃分,以下圖業務流程應該比較清晰,可是涉及一些術語,所以先把重要的術語定義清楚、下降你們的認知差別。
通用術語:
進件:金融領域術語,進件是指把資料準備好後提交給貸款公司或銀行的系統裏面,叫作進件,進件後銀行或貸款公司就會開始審覈這個貸款了。
上圖的1.0版本,銀行卡、進件、結算規則都跨越了問題域,所以咱們對其抽象「支付」「特約商戶」上下文,以下圖。
這裏有人會有疑問,「特約商戶」「商家」什麼關係,是否應該把「特約商戶」歸屬爲「商家域」,這只是字面意思的類似,「特約商戶」是進件審批之後造成的支付相關的業務。固然「商家域」會使用到「特約商戶」的能力。
由於進件邏輯複雜所以咱們以進件爲中心來畫出了這樣的上下文。另外一方面從狀態流轉來講,「銀行進件」是一個重要節點,表明平臺、商家的一些權益即將生效,所以以此爲核心也是有必要的。
隨着店鋪外賣團購業務的發展,咱們須要一個領域能力更豐富的履約安裝域,可以進行社區配送、售後維修等。不可避免地將與訂單、fa票、庫存、售後等業務都有關係,所以以訂單爲中心構建了下面的上下文。
考慮到篇幅以及內容繁多,領域層相關的內容會在後面的文章中繼續講解。
本文主要講述了戰略層面的DDD原則,相對來講較爲抽象,但這是最考驗內功、最不可忽視的環節。
再次強調一點,實踐DDD毫不是參照一套網上的代碼結構,依葫蘆畫瓢去重寫本身的系統,這必定是失敗的。建議你們按照本文所講述的原則、方法去思考本身的系統,當你領悟其精髓之後必定可以「笑傲代碼」,掌握解決軟件核心複雜性的內功心法。