天天看點

領域模型、貧血模型、充血模型概念總結

DDD(Domain Driven Design)早于微服務「出道」十年,這兩個「忘年交」的軟體設計哲學是如何相愛相殺的?

背景

微服務現在可以說是軟體研發領域無人不提的話題,然而業界流行的對比多數都是所謂的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中,我們對系統的劃分是基于領域(基于業務)的。比如上文提到支付平台是一個領域,而賬戶、會記、收款、付款等則為子領域。一個領域由衆多子領域聚集而形成。

當然,問題随之而來:

哪些概念應該模組化在哪些子系統裡面?

有時可能會發現一個領域概念模組化在子系統A中是可以的,而模組化在子系統B中也合情合理。

各個子系統之間的應該如何內建?

有人可能會說,這不簡單得就像用戶端調用服務端那麼簡單嗎?問題在于,兩個系統之間的內建涉及到基礎設施和不同領域概念在兩個系統之間的翻譯,稍不注意,這些概念就會對我們精心建立好的領域模型造成污染。

DDD中,有标準方法解決上述問題,就是限界上下文(Bounded Context)和上下文映射圖。在一個領域/子域中,我們會建立一個概念上的領域邊界,在這個邊界中,任何領域對象都隻表示特定于該邊界内部的确切含義。這樣的邊界便稱為限界上下文。限界上下文和領域具有一對一的關系。從實體層面講,一個限界上下文最終可以是一個Jar/War檔案,甚至可以是一個Package中的所有對象。但是,技術本身并不是用來界分限界上下文。

領域模型、貧血模型、充血模型概念總結

上圖引自《實作領域驅動設計》。通常情況下,一個領域有且隻有一個核心問題,我們稱之為該領域的「核心域」。在核心域、通用子域、支撐子域梳理的同時,會定義出子域中的「限界上下文」及其關系,用它來闡述子域之間的關系。界限上下文可以簡單了解成一個子系統或元件子產品。

DDD中的設計主要指領域模型的設計。DDD是一種基于模型驅動開發的軟體開發思想,強調領域模型是整個系統的核心,領域模型也是整個平台的核心價值。每一個領域都有一個對應的領域模型,領域模型能夠很好的解決負責的業務問題。是以領域模型的設計和架構設計同等重要。

DDD中,總是以領域為邊界,分析領域中的核心問題(核心關注點)。然後設計對應的領域模型,通過領域模型驅動代碼的實作。而資料庫設計、持久化技術這些都不是DDD的核心,屬于外圍的東西。與資料庫驅動開發的思路形成對比,驅動中需要記住兩個原則:

領域驅動領域模型設計

領域模型驅動代碼實作

領域驅動設計的最大價值是讓我們告别從面向過程式的思想(天馬星空,想到哪寫到哪)轉化為基于系統化的模型驅動思維。我們腦補一下軟體開發中的正常心路曆程:

1、設計表結構

2、寫代碼(代碼寫的很備援,不夠抽象)

3、維護代碼(适應業務變化)

4、遇到困難(資料結構設計不合理、代碼到處備援、改BUG引入新BUG、新人看代碼和無字天書一般)

5、愈發難以維護,開始重構(理論上在老基礎上改的技術債務堪比重新開發)

6、重構完成,新系統上線(相容曆史資料、資料遷移、新老系統并行,等等出發點考慮,其實本質上隻是做了代碼重構)

7、重複執行3-6步......

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;

防腐層:并非是系統間的消息傳遞機制,它的職責更具體的是指将某個模型或者契約中的概念對象及其行為轉換到另一個模型或者契約中;

讀完上面的兩種分層架構方式,可能很多人會有疑問,這些是什麼?為什麼我之前一直都沒聽到過這種分法?确實是這樣,DDD和面向對象、設計模式等等理論有千絲萬縷的聯系,如果不熟悉OOA、OOD,那麼DDD可能也會了解不了。因為我們大部分從開發生涯開始之初接觸的都是「Action層、Service層、Dao層、DB層」這樣的MVC分層理論。并且在21中設計模式中,「行為型」的設計模式,我們幾乎沒有什麼機會使用,導緻這些問題的原因是J2EE經典分層的開發方式是「貧血模型」。

Martin Fowler(對,就是提出微服務的那位大牛)曾經提出了兩種開發方式,即:

以「貧血模型」為基礎的「事務腳本」的開發方式

以「充血模型」為基礎的「領域驅動」的開發方式

貧血模型是指對象隻用于在各層之間傳輸資料使用,隻有資料字段和Get/Set方法,沒有邏輯在對象中。而「事務腳本」可以了解為業務是由一條條增删改查的SQL組織而成,是面向過程的程式設計。

充血模型是面向對象設計的本質,一個對象是擁有狀态和行為的。将大多數業務邏輯和持久化放在領域對象中,業務邏輯隻是完成對業務邏輯的封裝、事務、權限、校驗等的處理。

舉例,使用者管理子產品大概是這樣的兩種實作:

// 貧血模型下的實作

public class User{

}

public class UserManager{

public void save(User user){

// 儲存使用者的操作可能是這樣

userManager.save(user);

// 充血模型下的實作

private Integer id;

private String name;

...

// 省略get/set方法

user.save();

Martin Fowler定義的「貧血模型」是反模式,面對簡單的小系統用事務腳本方式開發沒問題;稍微大一些的系統使用事務腳本方式會擴大維護成本,業務邏輯、各種狀态散布在大量的函數中,哪怕就是要使用者對象中增加一個字段,可能都會涉及到幾個類的調整......

希望領域對象能夠準确地表達出業務意圖,但是多數時候,我們所看到的卻是充滿getter和setter的領域對象,此時的領域對象已經不是領域對象了,反模式的貧血對象了。其實在貧血模型和充血模型模型之外,還有失血模型和脹血模型,但後者兩個基本是實際開發中不會去使用,因為走的是兩個極端。

總結

本文宏觀角度介紹了領域驅動設計,那麼微服務和DDD是什麼關系呢?其實在2015年的一次演講中,DDD的提出者Eric Evans表達了對微服務技術的熱愛與支援,認為微服務是讓DDD落地的好工具。因為DDD和微服務其本質是降低軟體項目的複雜性,而DDD是一種設計理念/設計方法,DDD需要有強制性的原則做保障,否則不同的領域對象終究會混在一起。而微服務本身的一些限制,以及大家都能了解微服務的實施前提和首要條件,會在實作上給DDD增加了一些原則限制。DDD和微服務的不一定要同時使用落地,但是如果将DDD和微服務(兩個相差十歲的軟體設計方法)結合一起,那麼Martin Fowler和Eric Evans兩位布道師是會很贊同的。

繼續閱讀