DDD早於微服務「出道」十年,這兩個「忘年交」的軟體設計哲學是如何相愛相殺的? 背景 微服務現在可以說是軟體研發領域無人不提的話題,然而業界流行的對比多數都是所謂的Monolithic(單體應用),而大量的系統在十幾年前都已經是以SOA(面向服務架構)為基礎的分散式系統了,那麼微服務作為新的架構標準 ...
DDD早於微服務「出道」十年,這兩個「忘年交」的軟體設計哲學是如何相愛相殺的?
背景
微服務現在可以說是軟體研發領域無人不提的話題,然而業界流行的對比多數都是所謂的Monolithic(單體應用),而大量的系統在十幾年前都已經是以SOA(面向服務架構)為基礎的分散式系統了,那麼微服務作為新的架構標準與SOA有什麼差異點呢?其本質區別在於設計原理,微服務是去中心化設計,SOA是「集成」形成中心設計;
另外,筆者認為以下幾點並不是微服務和SOA的區別點:
CI/CD:持續集成、持續部署本身與敏捷、DevOps是交織在一起的,CI\CD更傾向於軟體工程的領域,與微服務無關;
- 基於容器還是虛擬機:Docker、虛擬機、物理機等是物理介質的一種實現方式,與微服務無關;
- 微服務周邊生態:比如日誌平臺、調用鏈系統?更多的是研發本身對於效率提高的自驅力,而與使用何種架構方式無關;
通訊協議:微服務的推薦通訊協議是RESTful,而傳統的SOA是SOAP。不過基於輕量級的RPC框架Dubbo、Thrift、gRPC來實現微服務也很多;在Spring Cloud中也有Feign框架將標準RESTful轉為代碼的API這種仿RPC的行為,這些通訊協議不是區分微服務架構和SOA架構的核心差別;
當然,軟體工程(DevOps)、基礎設施(容器化)、軟體開發模式(敏捷開發)的變革有利的推進了微服務架構的大行其道。而微服務架構是一種架構風格、架構理念,其中的「微」更體現了它的精髓在切分。在實際微服務的落地過程中證明,如果切分是錯誤的,你得不到微服務承諾的「低耦合、自治、易維護」之類的優勢,並且還會比單體架構擁有更多的麻煩。那麼如何切分呢?其實並不是一些新的方法論,而是都提出很多年的架構設計方法,也稱它們為微服務設計基礎或架構模型:領域驅動設計和立方體模型。
領域驅動設計
2004年,Eric Evans 發表了Domain Driven Design(領域驅動設計,DDD)。領域驅動設計已經問世十幾年,從Eric Evans出版的著作「領域驅動設計」一書中對領域驅動做了開創性的理論闡述,在軟體設計領域中,DDD可以稱得上是步入暮年時期了。遺憾的是,國外軟體圈享有盛譽並行之有效的設計方法學,國內大多數的技術人員卻並不瞭解,也未曾運用到項目實踐中。直到行業內吹起微服務的熱風,人們似乎才重新發現了領域驅動設計的價值,並不是微服務拯救了領域驅動設計,是因為領域驅動設計一直在頑強的成長,其設計開放的設計方法體系,雖然從來不曾在國內大行其道,但卻發揮著巨大的價值。錶面上看確實是因為微服務,領域驅動設計才又開始出現在大眾視野里。
領域驅動設計的意義
當然,領域驅動設計並非「銀彈」,不是能解決所有疑難雜症的「靈丹妙藥」,學習並應用它的意義在於:
- 一套完整的模型驅動的軟體設計方法,用於簡化軟體項目的複雜度,它能帶給你從戰略設計到戰術設計的規範過程,使得你的設計思路能夠更加清晰,設計過程更加規範;
- 一種思維方式和概念,可以應用在處理複雜業務的軟體項目中,加快項目的交付速度;
- 一組提煉出來的原則和模式,可以幫助開發者開發優雅的軟體系統、促進開發者對架構與模型的精心打磨,尤其善於處理系統架構的演進設計、有助於提高團隊成員的面向對象設計能力與架構設計能力;
領域驅動設計與微服務架構天生匹配,無論是在新項目中設計微服務架構,還是將系統從單體架構演進到微服務設計,都可以遵循領域驅動設計的架構原則。
當然,領域驅動能給我們帶來很多收穫,但如果你是屬於以下幾種情況的某種,那麼你確實不需要學習領域驅動設計了:- 如果你是獨當一面的架構師,並能設計出優雅的軟體架構
- 如果你是高效編碼的程式員,並只想踏踏實實的寫代碼
- 如果你是前端的設計人員,並奉行「用戶體驗至上」的理念
如果你負責的軟體系統並不複雜,二三人便可輕鬆維護
DDD的關鍵概念
一個軟體系統的誕生,一定是為瞭解決我們遇到的某個問題。比如一家企業的一直採用線下銷售產品,耗費大量的財力和物力,希望可以線上上銷售自己的產品,用於實現線上銷售銷售產品的目的,那麼就誕生了一個電商系統。通常最初設立的目標或要解決的問題就是一個軟體項目的出發點,明確我們要做什麼。比如一個電商、一個論壇、一個支付平臺等。
下文將從領域、問題域、領域模型、設計、驅動這幾個詞語的含義和聯繫的角度去闡述DDD是如何融入到軟體開發的。要理解什麼是領域驅動設計,首先要理解什麼是領域,什麼是設計,什麼是驅動,什麼驅動什麼。
什麼是領域/子領域(Domain/Subdomain)
領域是與某個特定問題相關的知識和行為。比如支付平臺就屬於特定的領域,只要是這個領域,都會有賬戶、會記、收款、付款、風控等核心環節。所以,同一個領域的系統都具有相同的核心業務,他們要解決的問題的本質是一致的。一個領域本質上可以理解為就是一個問題域,只要是同一個領域,那問題域就相同。所以,只要我們確定了系統所屬的領域,那這個系統的核心業務,即要解決的關鍵問題、問題的範圍邊界就基本確定了。
在日常開發中,我們通常會將一個大型的軟體系統拆分成若幹個子系統。這種劃分有可能是基於架構方面的考慮,也有可能是基於基礎設施的。在DDD中,我們對系統的劃分是基於領域(基於業務)的。比如上文提到支付平臺是一個領域,而賬戶、會記、收款、付款等則為子領域。一個領域由眾多子領域聚集而形成。
當然,問題隨之而來:
- 哪些概念應該建模在哪些子系統裡面?
- 有時可能會發現一個領域概念建模在子系統A中是可以的,而建模在子系統B中也合情合理。
- 各個子系統之間的應該如何集成?
- 有人可能會說,這不簡單得就像客戶端調用服務端那麼簡單嗎?問題在於,兩個系統之間的集成涉及到基礎設施和不同領域概念在兩個系統之間的翻譯,稍不註意,這些概念就會對我們精心創建好的領域模型造成污染。
DDD中,有標準方法解決上述問題,就是限界上下文(Bounded Context)和上下文映射圖。在一個領域/子域中,我們會創建一個概念上的領域邊界,在這個邊界中,任何領域對象都只表示特定於該邊界內部的確切含義。這樣的邊界便稱為限界上下文。限界上下文和領域具有一對一的關係。從物理層面講,一個限界上下文最終可以是一個Jar/War文件,甚至可以是一個Package中的所有對象。但是,技術本身並不是用來界分限界上下文。
上圖引自《實現領域驅動設計》。通常情況下,一個領域有且只有一個核心問題,我們稱之為該領域的「核心域」。在核心域、通用子域、支撐子域梳理的同時,會定義出子域中的「限界上下文」及其關係,用它來闡述子域之間的關係。界限上下文可以簡單理解成一個子系統或組件模塊。
什麼是設計(Design)
DDD中的設計主要指領域模型的設計。DDD是一種基於模型驅動開發的軟體開發思想,強調領域模型是整個系統的核心,領域模型也是整個平臺的核心價值。每一個領域都有一個對應的領域模型,領域模型能夠很好的解決負責的業務問題。所以領域模型的設計和架構設計同等重要。
什麼是驅動(Driven)
DDD中,總是以領域為邊界,分析領域中的核心問題(核心關註點)。然後設計對應的領域模型,通過領域模型驅動代碼的實現。而資料庫設計、持久化技術這些都不是DDD的核心,屬於外圍的東西。與資料庫驅動開發的思路形成對比,驅動中需要記住兩個原則:
- 領域驅動領域模型設計
領域模型驅動代碼實現
領域驅動設計的最大價值是讓我們告別從面向過程式的思想(天馬星空,想到哪寫到哪)轉化為基於系統化的模型驅動思維。我們腦補一下軟體開發中的常規心路歷程:- 1、設計表結構
- 2、寫代碼(代碼寫的很冗餘,不夠抽象)
- 3、維護代碼(適應業務變化)
- 4、遇到困難(數據結構設計不合理、代碼到處冗餘、改BUG引入新BUG、新人看代碼和無字天書一般)
- 5、愈發難以維護,開始重構(理論上在老基礎上改的技術債務堪比重新開發)
- 6、重構完成,新系統上線(相容歷史數據、數據遷移、新老系統並行,等等出發點考慮,其實本質上只是做了代碼重構)
7、重覆執行3-6步......
DDD的分層架構
四層架構
Eric Evans在《領域驅動設計-軟體核心複雜性應對之道》這本書中提出了傳統的四層架構模式,在後來演進過程中出現了五層架構和六層架構,,如下圖所示:
- User Interface:用戶界面層/展示層,負責與用戶交互。包含顯示信息、解釋用戶命令等;
- Application:應用層,用來協調用戶與各應用以及各應用之間的交互。不包含業務邏輯、不保存業務對象的狀態;
- Domain:領域層/模型層,負責表達業務概念,業務狀態信息以及業務規則。包含領域模型、領域信息、業務對象的狀態。領域層是業務軟體的核心;
- Infrastructure:基礎設施層,為其他各層提供技術能力。包括為應用層傳遞消息、為領域層提供持久化機制、為用戶界面層繪製屏幕組件等等。基礎設施層還能夠通過架構框架來支持四個層次間的交互模式。
六邊形架構
隨著後續的演進,出現了一種改進分層架構的方法,即Robert C. Martin提出的依賴倒置原則(Dependency Inversion Principle,DIP)。它通過改變不同層之間的依賴關係達到改進目的。
- 高層模塊不應該依賴於底層模塊,兩者都應該依賴於抽象
- 抽象不應該依賴於細節,細節應該依賴於抽象
根據該原則的定義,DDD分層架構中的低層組件應該依賴於高層組件提供的介面,即無論高層還是低層都依賴於抽象,整個分層架構好像被推平了,再向其中加入了一些對稱性,就出現了一種具有對稱性特征的六邊形架構風格。六邊形架構是Alistair Cockburn在2005年提出的,其本質是倡導不同的客戶通過「平等」的方式與系統交互,通過不斷的擴展適配器轉化成系統API所理解的參數來達到每種特定的輸出,而每種特定的輸出都有適配器完成相應的轉化功能。
聚合:
- 一組具有內聚關係的相關對象的集合;
- – 是一個修改數據的最小原子單元;
- – 聚合通常使用id訪問;
- 實體(Entity):表示具有生命周期並且會在其生命周期中發生改變的東西。含有VO、具有identity的特性,通常具有生命周期的概念 JPA tag @Entity;
- 值對象(Value Object):表示起描述性作用的並且可以相互替換的概念。類似於pojo,不可變immutable,可在不同模型中傳遞,Spring tag @value;
- 領域事件(Domain Event):所有的領域對象的跨聚合變更需要以事件方式進行通知和記錄,聚合內的酌情考慮;
- 工廠(Factory):負責所有對象的生成和組裝;
- 領域服務(Domain Service):純技術層面的服務,例如日誌,或者是跨聚合的編排服務,通常是Spring Component;
- 資源層(Repository):類似於DAO層,Spring JPA, Hibernate之類 @CRUDRepository;
- 防腐層:並非是系統間的消息傳遞機制,它的職責更具體的是指將某個模型或者契約中的概念對象及其行為轉換到另一個模型或者契約中;
貧血模型VS充血模型
讀完上面的兩種分層架構方式,可能很多人會有疑問,這些是什麼?為什麼我之前一直都沒聽到過這種分法?確實是這樣,DDD和麵向對象、設計模式等等理論有千絲萬縷的聯繫,如果不熟悉OOA、OOD,那麼DDD可能也會理解不了。因為我們大部分從開發生涯開始之初接觸的都是「Action層、Service層、Dao層、DB層」這樣的MVC分層理論。並且在21中設計模式中,「行為型」的設計模式,我們幾乎沒有什麼機會使用,導致這些問題的原因是J2EE經典分層的開發方式是「貧血模型」。
Martin Fowler(對,就是提出微服務的那位大牛)曾經提出了兩種開發方式,即:
- 以「貧血模型」為基礎的「事務腳本」的開發方式
以「充血模型」為基礎的「領域驅動」的開發方式
貧血模型
貧血模型是指對象只用於在各層之間傳輸數據使用,只有數據欄位和Get/Set方法,沒有邏輯在對象中。而「事務腳本」可以理解為業務是由一條條增刪改查的SQL組織而成,是面向過程的編程。
充血模型是面向對象設計的本質,一個對象是擁有狀態和行為的。將大多數業務邏輯和持久化放在領域對象中,業務邏輯只是完成對業務邏輯的封裝、事務、許可權、校驗等的處理。
舉例,用戶管理模塊大概是這樣的兩種實現:
// 貧血模型下的實現
public class User{
private Integer id;
private String name;
...
// 省略get/set方法
}
public class UserManager{
public void save(User user){
// 持久化操作....
}
}
// 保存用戶的操作可能是這樣
userManager.save(user);
// 充血模型下的實現
public class User{
private Integer id;
private String name;
...
// 省略get/set方法
public void save(){
// 持久化操作....
}
}
// 保存用戶的操作可能是這樣
user.save();
Martin Fowler定義的「貧血模型」是反模式,面對簡單的小系統用事務腳本方式開發沒問題;稍微大一些的系統使用事務腳本方式會擴大維護成本,業務邏輯、各種狀態散佈在大量的函數中,哪怕就是要用戶對象中增加一個欄位,可能都會涉及到幾個類的調整......
希望領域對象能夠準確地表達出業務意圖,但是多數時候,我們所看到的卻是充滿getter和setter的領域對象,此時的領域對象已經不是領域對象了,反模式的貧血對象了。其實在貧血模型和充血模型模型之外,還有失血模型和脹血模型,但後者兩個基本是實際開發中不會去使用,因為走的是兩個極端。
總結
本文巨集觀角度介紹了領域驅動設計,那麼微服務和DDD是什麼關係呢?其實在2015年的一次演講中,DDD的提出者Eric Evans表達了對微服務技術的熱愛與支持,認為微服務是讓DDD落地的好工具。因為DDD和微服務其本質是降低軟體項目的複雜性,而DDD是一種設計理念/設計方法,DDD需要有強制性的原則做保障,否則不同的領域對象終究會混在一起。而微服務本身的一些限制,以及大家都能理解微服務的實施前提和首要條件,會在實現上給DDD增加了一些原則限制。DDD和微服務的不一定要同時使用落地,但是如果將DDD和微服務(兩個相差十歲的軟體設計方法)結合一起,那麼Martin Fowler和Eric Evans兩位佈道師是會很贊同的。