2-5 萬字講解 DDD 領域驅動設計,從理論到實踐掌握 DDD 分層架構設計,趕緊收藏起來吧!

1、背景

1.1. 前言

小編先問大家一個問題,也算是高級工程師面試中常問的問題,怎麼樣才能設計出一個好的軟件系統,或者說一個高質量的大型軟件系統應該具有哪些特點?
歡迎大家在評論區積極留言探討,或者關注【微信公衆號】微信搜索【老闆來一杯 java】回覆【進羣】即可進入無廣告交流羣!回覆【java】即可獲取【java 基礎經典面試】一份!加羣后即可獲取【DDD 領域驅動設計實戰落地解惑】PDF 一份!

DDD 也是設計一個高質量軟件系統中的一種解決方案。瞭解 DDD 之前,小編建議讀者還是需要具備一定的設計模式的思想,不太瞭解設計模式的可以先參考小編的文章:
2.5 萬字詳解 23 種設計模式:

https://blog.csdn.net/qq_41889508/article/details/105953114?spm=1001.2014.3001.5501

DDD 這個思想呢,最早是 Eric Evans(埃裏克 · 埃文斯)在 2003 年《Domain-Driven Design –Tackling Complexity in the Heart of Software》書中提出的一個概念,該書翻譯過來就是領域驅動設計—軟件核心複雜性應對之道,但是提出的時候微服務當時並沒有流行,所以一直沒有火起來,DDD 最近開始流行的原因,主要是藉着微服務的東風。

1.2. MVC 模式 VS DDD 模式

MVC 三層開發模式大家應該都非常熟悉,現在公司開發基本都是這種模式。

MVC 開發流程:

  1. 用戶需求轉化爲產品需求

  2. 需求評審會 pm 講解需求轉化爲研發需求

  3. 研發人員根據需求進行設計庫表結構

  4. 編寫 dao 層代碼

  5. 編寫 service 代碼

  6. 編寫 controller 代碼

這一步步下來,是不是感覺非常的絲滑。比如產品提了一個需求,首先我們一般會想考慮設計幾張表,怎麼存儲數據,然後建立 dao 層,service 層,controller 層來實現這個功能。但是嚴格來講,mvc 本質上是一種面向數據的設計,主要關注數據,自低向上的思想。雖然在開發速度上有一定優勢,如果只追求開發速度,面向數據模型編程在短期之內可以搞定需求,但一味追求速度,如果你係統的業務變化快速,從長遠來看隨着時間的增長,系統堆了雜七雜八以後,MVC 的短板就會日益明顯。

1.2.1. MVC 存在的問題:

  1. 新需求的開發會越來越難。
  2. 代碼維護越來越難,一個類代碼太多,這怎麼看對吧,就是一堆屎山。
  3. 技術創新越來越難,代碼沒時間重構,越拖越爛。
  4. 測試越來越難,沒辦法單元測試,一個小需求又要回歸測試,太累。

1.2.2. 使用 DDD 的意義:

單體架構局部業務膨脹可以拆分成微服務,微服務架構局部業務膨脹,又拆成什麼呢?
DDD 就是爲了解決這些問題的存在,從一個軟件系統的長期價值來看,就需要用 DDD,雖然一開始從設計到開發需要成本,但是隨着時間的增長,N 年以後代碼依然很整潔,利於擴展和維護,高度自治,高度內聚,邊界領域劃分的很清楚。當然了,針對於簡單的系統用 DDD 反而用複雜了,殺雞焉用宰牛刀!
MVC 的開發模式:是數據驅動,自低向上的思想,關注數據。
DDD 的開發模式:是領域驅動,自頂向下,關注業務活動。

1.3. 總結:

DDD 分層架構中的要素其實和三層架構類似,只是在 DDD 分層架構中,這些要素被重新歸類,重新劃分了層,確定了層與層之間的交互規則和職責邊界。
MVC 是一個短暫的快樂但不足以支撐漫長的生活,DDD 是一個不要短暫的溫存而是一世的陪伴,如果是你來抉擇你會選擇哪一個?

2、DDD 領域驅動模型

2.1. 概念

1. DDD:

DDD(Domain Driven Design)領域驅動模型,是一種處理高度複雜領域的設計思想,不是一種架構,而是一種架構設計方法論,是一種設計模式。說白了就是把一個複雜的軟件應用系統的其中各個部分進行一個很好的拆解和封裝,以達到高內聚低耦合的這樣一個效果。

說白了就是,DDD 就是以高內聚低耦合爲目的,對軟件系統進行模塊化的一種思想。

2. 戰略設計:

指的是領域名詞、動詞分析、提取領域模型。官方解釋,在某個領域,核心圍繞上下文的設計,主要關注上下文的劃分、上下文映射的設計,通用語言的設計。

說白了就是,在某個系統,核心圍繞子系統的設計;主要關注,這些子系統的劃分,子系統的交互方式,還有子系統的核心術語的定義。

3. 戰術設計:

用領域模型指導設計及編碼的實現。官方解釋,核心關注上下文中的實體建模,定義值對象,實體等,更偏向開發細節。

說白了就是,上下文對應的就是某一個子系統,子系統裏代碼實現怎麼設計,就是戰術設計要解決的問題。核心關注某個子系統的代碼實現,以面向對象的思維設計類的屬性和方法,和設計類圖沒有什麼區別,只是有一些規則而已。就是
指導我們劃分類。

4. 問題空間:

問題空間屬於需求分析階段,重點是明確這個系統要解決什麼問題,能夠提供什麼價值,也就是關注系統的 What 與 Why。
問題空間將問題域提煉成更多可管理的子域,是真對於問題域而言的。問題空間
在研究和解決業務問題時,DDD 會按照一定的規則將業務領域進行細分,當領域細分到一定的程度後,DDD 會將問題範圍限定在特定的邊界內,在這個邊界內建立領域模型,進而用代碼實現該領域模型,解決相應的業務問題。簡言之,DDD 的領域就是這個邊界內要解決的業務問題域。

領域可以進一步劃分爲子領域。我們把劃分出來的多個子領域稱爲子域,每個子域對應一個更小的問題域或更小的業務範圍,每個子域又包含了核心子域,支撐子域,通用子域。

領域的核心思想就是將問題域逐級細分,來降低業務理解和系統實現的複雜度。通過領域細分,逐步縮小服務需要解決的問題域,構建合適的領域模型。

說白了就是,就是系統下面有多個子系統,就是分了一些類型,比如電商系統,訂單就是核心領域,支付調用銀行,支付寶什麼的就是支撐子域,相當於我們俗稱的下游,通用子域,就是一些鑑權,用戶中心,每個系統都會用到,就設計成通用子域,關鍵就是討論過程如何得出這些問題域,是戰略設計要解決的。

5. 解決空間:

解決方案域屬於系統設計階段,針對識別出來的問題域,尋求合理的解決方案,也就是關注系統的 How。在領域驅動設計中,核心領域(Core Domain)與子領域(Sub Domain)屬於問題域的範疇,限界上下文(Bounded Context)則屬於解決方案域的範疇。
說白了就是,得出這些問題域之後,就基於這些問題域來求解,屬於解決空間。相當於,知道了 y=2x, 知道了 x 是多少,然後求 y 的值。解決空間就是指,領域之間的關係是什麼樣子,每個領域中通用的術語 ,具體在領域內怎麼實現代碼,進行領域建模就可以了。
從問題域到解決方案域,實際上就是從需求分析到設計的過程,也是我們逐步識別限界上下文的過程。

6. 事件風暴:

事件風暴的基本思想,就是將軟件開發人員和領域專家聚集在一起,完成領域模型設計(領域分析和領域建模)。劃分出微服務邏輯邊界和物理邊界,定義領域模型中的領域對象,指導微服務設計和開發。

領域分析,是根據需求劃分出初步的領域和限界上下文,以及上下文之間的關係;然後分析每個上下文內部,抽取每個子域的領域概念,識別出哪些是實體,哪些是值對象;

領域建模,就是對實體、值對象進行關聯和聚合,劃分出聚合的範疇和聚合根;

DDD 需要進行領域分析和領域建模,除了事件風暴之外實現的方法有,領域故事講述,四色建模法,用例法等。

事件風暴是建立領域模型的主要方法,但是在 DDD 領域建模和系統建設過程中,有很多的參與者,包括領域專家、產品經理、項目經理、架構師、開發經理和測試經理等。對同樣的領域知識,不同的參與角色可能會有不同的理解,那大家交流起來就會有障礙,怎麼辦呢?因此,在 DDD 中就出現了 “通用語言” 和“限界上下文”這兩個重要的概念。

7. 通用語言:

DDD 的主要參與者:領域專家 + 開發人員。領域專家擅長某個領域的知識,專注於交付的業務價值。而開發人員則注重於技術實現,總是想着類、接口、方法、設計模式、架構等。這也就導致了團隊交流的困難性。因此找到雙方的通用語言是解決該問題的有效途徑。

通用語言定義上下文含義。在事件風暴過程中,通過團隊交流達成共識的,能夠簡單、清晰、準確描述業務涵義和規則的語言就是通用語言
通用語言包含術語和用例場景,並且能夠直接反映在代碼中。通用語言中的名詞可以給領域對象命名,如商品、訂單等,對應實體對象;而動詞則表示一個動作或事件,如商品已下單、訂單已付款等,對應領域事件或者命令。

通用語言說白了就是,使用團隊中大家都懂的概念,解決交流障礙的問題,使領域專家和開發人員能夠協同合作,從而能夠確保業務需求的正確表達。

8. 限界上下文:

官方解釋:限界上下文主要用來封裝通用語言和領域對象。
限界上下文可以拆分爲兩個詞,限界和上下文。

限界:適用的對象一般是抽象事物,指不同事物的分界,指定某些事物的範圍。

上下文:個人理解就是語境。語言都有它的語義環境,同樣,通用語言也有它的上下文環境。爲了避免同樣的概念或語義在不同的上下文環境中產生歧義,DDD 在戰略設計上提出了 “限界上下文” 這個概念,用來確定語義所在的領域邊界。限界上下文就是用來定義領域邊界,以確保每個上下文含義在它特定的邊界內都具有唯一的含義,領域模型則存在於這個邊界之內。這個邊界定義了模型的適用範圍,使團隊所有成員能夠明確地知道什麼應該在模型中實現,什麼不應該在模型中實現。
比如說,商品在不同的階段有不同的術語,在銷售階段是商品,而在運輸階段則變成了貨物。同樣的一個東西,由於業務領域的不同,賦予了這些術語不同的涵義和職責邊界,這個邊界就可能會成爲未來微服務設計的邊界。那麼,領域邊界就是通過限界上下文來定義的。

限界上下文是微服務設計和拆分的主要依據。在領域模型中,如果不考慮技術異構、團隊溝通等其它外部因素,一個限界上下文理論上就可以設計爲一個微服務。

限界上下文是業務概念的邊界,是業務問題最小粒度的劃分。在某個業務領域中會包含多個限界上下文,我們通過找出這些確定的限界上下文對系統進行解耦,要求每一個限界上下文其內部必須是緊密組織的、職責明確的、具有較高的內聚性。說白了就是,上下文對應的就是某一個子系統,系統之間要劃分好邊界。

9. 上下文映射:

上下文之間交互方式就是上下文映射,相對於系統裏面這就是 RPC,http 等交互方式。

10. 領域:

從廣義上講,領域具體指一種特定的範圍或區域。在 DDD 中上下文的劃分完的東西叫作領域,領域下面又劃分了,核心領域,支撐子域,通用子域。
子域:在領域不斷劃分的過程中,領域會細分爲不同的子域,子域可以根據自身重要性和功能屬性劃分爲三類子域,它們分別是:核心域、通用域和支撐域。

核心域:它是業務成功的主要因素和公司的核心競爭力
通用域:沒有太多個性化的訴求,同時被多個子域使用的通用功能子域是通用域
支撐域:有一種功能子域是必需的,但既不包含決定產品和公司核心競爭力的功能,也不包含通用功能的子域,就是支撐域。

說白了就是,系統下面有多個子系統,就是分了一些類型,比如電商系統,訂單就是核心領域,支付調用銀行,支付寶什麼的就是支撐子域,相當於我們俗稱的下游,通用子域,就是一些鑑權,用戶中心,每個系統都會用到,就設計成通用子域,關鍵就是討論過程如何得出這些域,是戰略設計要解決的。

11. 領域模型:

領域模型是對領域內的概念類或現實世界中對象的可視化表示。它專注於分析問題領域本身,發掘重要的業務領域概念,並建立業務領域概念之間的關係。
是描述業務用例實現的對象模型。它是對業務角色和業務實體之間應該如何聯繫和協作以執行業務的一種抽象。
領域模型分爲領域對象和領域服務兩大類,領域對象用於存儲狀態,領域服務用於改變領域對象的狀態。

特點:

  1. 領域模型是對具有某個邊界的領域的一個抽象,反映了領域內用戶業務需求的本質;領域模型是有邊界的,只反應了我們在領域內所關注的部分;

  2. 領域模型只反映業務,和任何技術實現無關;領域模型不僅能反映領域中的一些實體概念,如貨物,書本,應聘記錄,地址,等;還能反映領域中的一些過程概念,如資金轉賬,等;

  3. 領域模型確保了我們的軟件的業務邏輯都在一個模型中,都在一個地方;這樣對提高軟件的可維護性,業務可理解性以及可重用性方面都有很好的幫助;

  4. 領域模型能夠幫助開發人員相對平滑地將領域知識轉化爲軟件構造;

  5. 領域模型貫穿軟件分析、設計,以及開發的整個過程;領域專家、設計人員、開發人員通過領域模型進行交流,彼此共享知識與信息;因爲大家面向的都是同一個模型,所以可以防止需求走樣,可以讓軟件設計開發人員做出來的軟件真正滿足需求;

  6. 要建立正確的領域模型並不簡單,需要領域專家、設計、開發人員積極溝通共同努力,然後才能使大家對領域的認識不斷深入,從而不斷細化和完善領域模型;

  7. 爲了讓領域模型看的見,我們需要用一些方法來表示它;圖是表達領域模型最常用的方式,但不是唯一的表達方式,代碼或文字描述也能表達領域模型;

  8. 領域模型是整個軟件的核心,是軟件中最有價值和最具競爭力的部分;設計足夠精良且符合業務需求的領域模型能夠更快速的響應需求變化;

12. 領域事件:

聚合之間產生的業務協同使用領域事件的方式來完成,領域事件就是將上游聚合處理完成這個動作通過事件的方式進行抽象。

在 DDD 中有一個原則,一個業務用例對應一個事務,一個事務對應一個聚合根,也就是在一次事務中只能對一個聚合根操作。但在實際應用中,一個業務用例往往需要修改多個聚合根,而不同的聚合根可能在不同的限界上下文中,引入領域事件即不破壞 DDD 的一個事務只修改一個聚合根的原則,也能實現限界上下文之間的解耦。對於領域事件發佈,在領域服務發佈,在不使用領域服務的情況下,則由應用層在調用資源庫持久化聚合根之後再發布領域事件。

一個事件可能當前限界上下文內也需要消費,即可能有多個限界上下文需要消費,一個事件對應多個消費者。

一個完整的領域事件 = 事件發佈 + 事件存儲 + 事件分發 + 事件處理。
事件發佈:構建一個事件,需要唯一標識,然後發佈;
事件存儲:發佈事件前需要存儲,因爲接收後的事建也會存儲,可用於重試或對賬等;就是每次執行一次具體的操作時,把行爲記錄下來,執行持久化。
事件分發:服務內的應用服務或者領域服務直接發佈給訂閱者,服務外需要藉助消息中間件,比如 Kafka,RabbitMQ 等,支持同步或者異步。
事件處理:先將事件存儲,然後再處理。
當然了,實際開發中事件存儲和事件處理不是必須的。

因此實現方案:發佈訂閱模式,分爲跨上下文(kafka,RocketMq)和上下文內(spring 事件,Guava Event Bus)的領域事件。

e.g. 用戶註冊後,發送短信和郵件,使用 spring 事件實現領域事件代碼如下:

  1. 創建用戶註冊事件
/**
 * 用戶註冊事件
 * @Author WDYin
 **/public class UserRegisterEvent extends ApplicationEvent {
    
    public UserRegisterEvent(Object source) {
        super(source);
    }}
  1. 用戶監聽事件
/**
 * 用戶監聽事件
 * @Author WDYin
 **/@Componentpublic class UserListener {

    @EventListener(UserRegisterEvent.class)
    public void userRegister(UserRegisterEvent event) {
        User user = (User) event.getSource();
        System.out.println("用戶註冊。。。發送短信。。。" + user);
        System.out.println("用戶註冊。。。發送郵件。。。" + user);
    }

    @EventListener(UserCancelEvent.class)
    public void userCancelEvent(UserCancelEvent event) {
        User user = (User) event.getSource();
        System.out.println("用戶註銷。。。" + user);
    }
    }
  1. 發佈用戶註冊事件
/**
 * 發佈用戶註冊事件
 * @Author : WDYin
 */@RunWith(value = SpringJUnit4ClassRunner.class)@SpringBootTest(classes = DemoApplication.class)public class MyClient {

    @Autowired
    private ApplicationContext applicationContext;

    @Test
    public void test() {
        User user = new User();
        //發佈事件
        applicationContext.publishEvent(new UserRegisterEvent(user));
    }}

13. 實體:

官方解釋,實體和值對象會形成聚合,每個聚合一般是在一個事務中操作,一般都有持久性操作。聚合中,跟實體的生命週期決定了聚合整體的生命週期。說白了,就是對象之間的關聯,只是規定了關聯對象規則(必須是由實體和值對象組成的),操作聚合時類似 hibernate 的 One-Many 對象的操作,一起操作,不能單獨操作。

e.g. 權限管理系統——用戶實體,代碼如下:

@NoArgsConstructor@Getterpublic class User extends Aggregate<Long, User> {

    /**
     * 用戶id-聚合根唯一標識
     */
    private UserId userId;

    /**
     * 用戶名
     */
    private String userName;

    /**
     * 姓名
     */
    private String realName;

    /**
     * 手機號
     */
    private String phone;

    /**
     * 密碼
     */
    private String password;

    /**
     * 鎖定結束時間
     */
    private Date lockEndTime;

    /**
     * 登錄失敗次數
     */
    private Integer failNumber;

    /**
     * 用戶角色
     */
    private List<Role> roles;

    /**
     * 部門
     */
    private Department department;

    /**
     * 領導
     */
    private User leader;

    /**
     * 下屬
     */
    private List<User> subordinationList = new ArrayList<>();

    /**
     * 用戶狀態
     */
    private UserStatus userStatus;

    /**
     * 用戶地址
     */
    private Address address;

    public User(String userName, String phone, String password) {

        saveUserName(userName);
        savePhone(phone);
        savePassword(password);
    }

    /**
     * 保存用戶名
     * @param userName
     */
    private void saveUserName(String userName) {
        if (StringUtils.isBlank(userName)){
            Assert.throwException("用戶名不能爲空!");
        }

        this.userName = userName;
    }

    /**
     * 保存電話
     * @param phone
     */
    private void savePhone(String phone) {
        if (StringUtils.isBlank(phone)){
            Assert.throwException("電話不能爲空!");
        }

        this.phone = phone;
    }

    /**
     * 保存密碼
     * @param password
     */
    private void savePassword(String password) {
        if (StringUtils.isBlank(password)){
            Assert.throwException("密碼不能爲空!");
        }

        this.password = password;
    }

    /**
     * 保存用戶地址
     * @param province
     * @param city
     * @param region
     */
    public void saveAddress(String province,String city,String region){
        this.address = new Address(province,city,region);
    }

    /**
     * 保存用戶角色
     * @param roleList
     */
    public void saveRole(List<Role> roleList) {

        if (CollectionUtils.isEmpty(roles)){
            Assert.throwException("角色不能爲空!");
        }

        this.roles = roleList;
    }

    /**
     * 保存領導
     * @param leader
     */
    public void saveLeader(User leader) {
        if (Objects.isNull(leader)){
            Assert.throwException("leader不能爲空!");
        }
        this.leader = leader;
    }

    /**
     * 增加下屬
     * @param user
     */
    public void increaseSubordination(User user) {

        if (null == user){
            Assert.throwException("leader不能爲空!");
        }

        this.subordinationList.add(user);
    }}

###14. 值對象:
官方解釋,描述了領域中的一件東西,將不同的相關屬性組合成了一個概念整體,當度量和描述改變時,可以用另外一個值對象予以替換,屬性判等,固定不變。
說白了就是,不關心唯一值,具有校驗邏輯,等值判斷邏輯,只關心值的類。只有數據初始化操作和有限的不涉及修改數據的行爲,基本不包含業務邏輯。比如下單的地址。

當你決定一個領域概念是否是一個值對象時,需考慮它是否擁有以下特徵:

  1. 度量或者描述了領域中的一件東西

  2. 可作爲不變量

  3. 將不同的相關的屬性組合成一個概念整體 (Conceptual Whole)

  4. 當度量和描述改變時,可以用另一個值對象予以替換

  5. 可以和其他值對象進行相等性比較

  6. 不會對協作對象造成副作用

  7. 當你只關心某個對象的屬性時,該對象便可作爲一個值對象。爲其添加有意義的屬性,並賦予它相應的行爲。需要將值對象看成不變對象,不要給它任何身份標識, 還應儘量避免像實體對象一樣的複雜性。

值對象本質上就是一個集。該集合有若干用於描述目的、具有整體概念和不可修改的屬性。該集合存在的意義是在領域建模的過程中,值對象可保證屬性歸類的清晰和概念的完整性,避免屬性零碎。

代碼如下

/**
 * 地址數據
 *
 * @Author WDYin
 * @Date 2022/5/24
 */@Getterpublic class Address extends ValueObject {
    /**
     * 省
     */
    private String province;

    /**
     * 市
     */
    private String city;

    /**
     * 區
     */
    private String region;

    public Address(String province, String city, String region) {
        if (StringUtils.isBlank(province)){
            Assert.throwException("province不能爲空!");
        }
        if (StringUtils.isBlank(city)){
            Assert.throwException("city不能爲空!");
        }
        if (StringUtils.isBlank(region)){
            Assert.throwException("region不能爲空!");

        }
        this.province = province;
        this.city = city;
        this.region = region;
    }}

14. 聚合:

官方解釋,實體和值對象會形成聚合,每個聚合一般是在一個事務中操作,一般都有持久性操作。聚合中,跟實體的生命週期決定了聚合整體的生命週期。說白了,就是對象之間的關聯,只是規定了關聯對象規則(必須是由實體和值對象組成的),操作聚合時類似 hibernate 的 One-Many 對象的操作,一起操作,不能單獨操作。

聚合的規範:

  1. 我們把一些關聯性極強、生命週期一致的實體、值對象放到一個聚合裏。

  2. 聚合是領域對象的顯式分組,旨在支持領域模型的行爲和不變性,同時充當一致性和事務性邊界。

  3. 聚合在 DDD 分層架構裏屬於領域層,領域層包含了多個聚合,共同實現核心業務邏輯。跨多個實體的業務邏輯通過領域服務來實現,跨多個聚合的業務邏輯通過應用服務來實現。

  4. 比如有的業務場景需要同一個聚合的 A 和 B 兩個實體來共同完成,我們就可以將這段業務邏輯用領域服務來實現;而有的業務邏輯需要聚合 C 和聚合 D 中的兩個服務共同完成,這時你就可以用應用服務來組合這兩個服務。

在 DDD 中,聚合也可以用來表示整體與部分的關係,但不再強調部分與整體的獨立性。聚合是將相關聯的領域對象進行顯示分組,來表達整體的概念(也可以是單一的領域對象)。比如將表示訂單與訂單項的領域對象進行組合,來表達領域中訂單這個整體概念。

15. 聚合根:

聚合根 (Aggreate Root, AR) 就是軟件模型中那些最重要的以名詞形式存在的領域對象。聚合根是主要的業務邏輯載體,DDD 中所有的戰術實現都圍繞着聚合根展開。70% 的場景下,一個聚合內都只有一個實體,那就是聚合根。
說白了就是:聚合的根實體,最具代表性的實體。比如訂單和訂單項聚合之後的聚合根就是訂單。

聚合根的特徵

  1. 它作爲實體本身,擁有實體的屬性和業務行爲,實現自身的業務邏輯。

  2. 它作爲聚合的管理者,在聚合內部負責協調實體和值對象按照固定的業務規則協同完成共同的業務邏輯。

  3. 聚合根之間的引用通過 ID 完成。在聚合之間,它還是聚合對外的接口人,以聚合根 ID 關聯的方式接受外部任務和請求,在上下文內實現聚合之間的業務協同。也就是說,聚合之間通過聚合根 ID 關聯引用,如果需要訪問其它聚合的實體,就要先訪問聚合根,再導航到聚合內部實體,外部對象不能直接訪問聚合內實體。

    簡單概括一下:

通過事件風暴(我理解就是頭腦風暴,不過我們一般都是先通過個人理解,然後再和相關核心同學進行溝通),得到實體和值對象;
將這些實體和值對象聚合爲 “投保聚合” 和“客戶聚合”,其中 “投保單” 和“客戶”是兩者的聚合根;
找出與聚合根 “投保單” 和“客戶”關聯的所有緊密依賴的實體和值對象;
在聚合內根據聚合根、實體和值對象的依賴關係,畫出對象的引用和依賴模型。

16. 貧血模型:

貧血模型具有一堆屬性和 set get 方法,存在的問題就是通過 pojo 這個對象上看不出業務有哪些邏輯,一個 pojo 可能被多個模塊調用,只能去上層各種各樣的 service 來調用,這樣以後當梳理這個實體有什麼業務,只能一層一層去搜 service,也就是貧血失憶症,不夠面向對象。

代碼如下

public class User {

    private Long id;
    private String userName;//用戶名
    private  String password;//密碼
    private  String gesture;    //手勢密碼
    private  String phone;    //手機號碼
    private String email;
    private  int status;    //賬戶狀態
    private Date lockEndTime;    //鎖定結束時間
    private int failNumber;    //登錄失敗次數

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getUserName() {
        return userName;
    }

    public void setUserName(String userName) {
        this.userName = userName;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public String getGesture() {
        return gesture;
    }

    public void setGesture(String gesture) {
        this.gesture = gesture;
    }

    public String getPhone() {
        return phone;
    }

    public void setPhone(String phone) {
        this.phone = phone;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    public int getStatus() {
        return status;
    }

    public void setStatus(int status) {
        this.status = status;
    }

    public Date getLockEndTime() {
        return lockEndTime;
    }

    public void setLockEndTime(Date lockEndTime) {
        this.lockEndTime = lockEndTime;
    }

    public int getFailNumber() {
        return failNumber;
    }

    public void setFailNumber(int failNumber) {
        this.failNumber = failNumber;
    }}

17. 充血模型:

比如如下 user 用戶有改密碼,改手機號,修改登錄失敗次數等操作,都內聚在這個 user 實體中,每個實體的業務都是清晰的,就是充血模型,充血模型的內存計算會多一些,內聚核心業務邏輯處理。

說白了就是,不只是有貧血模型中 setter getter 方法,還有其他的一些業務方法,這纔是面向對象的本質,通過 user 實體就能看出有哪些業務存在。

代碼如下

@NoArgsConstructor@Getterpublic class User extends Aggregate<Long, User> {

    /**
     * 用戶名
     */
    private String userName;

    /**
     * 姓名
     */
    private String realName;

    /**
     * 手機號
     */
    private String phone;

    /**
     * 密碼
     */
    private String password;

    /**
     * 鎖定結束時間
     */
    private Date lockEndTime;

    /**
     * 登錄失敗次數
     */
    private Integer failNumber;

    /**
     * 用戶角色
     */
    private List<Role> roles;

    /**
     * 部門
     */
    private Department department;

    /**
     * 用戶狀態
     */
    private UserStatus userStatus;

    /**
     * 用戶地址
     */
    private Address address;

    public User(String userName, String phone, String password) {

        saveUserName(userName);
        savePhone(phone);
        savePassword(password);
    }

    /**
     * 保存用戶名
     * @param userName
     */
    private void saveUserName(String userName) {
        if (StringUtils.isBlank(userName)){
            Assert.throwException("用戶名不能爲空!");
        }

        this.userName = userName;
    }

    /**
     * 保存電話
     * @param phone
     */
    private void savePhone(String phone) {
        if (StringUtils.isBlank(phone)){
            Assert.throwException("電話不能爲空!");
        }

        this.phone = phone;
    }

    /**
     * 保存密碼
     * @param password
     */
    private void savePassword(String password) {
        if (StringUtils.isBlank(password)){
            Assert.throwException("密碼不能爲空!");
        }

        this.password = password;
    }

    /**
     * 保存用戶地址
     * @param province
     * @param city
     * @param region
     */
    public void saveAddress(String province,String city,String region){
        this.address = new Address(province,city,region);
    }

    /**
     * 保存用戶角色
     * @param roleList
     */
    public void saveRole(List<Role> roleList) {

        if (CollectionUtils.isEmpty(roles)){
            Assert.throwException("角色不能爲空!");
        }

        this.roles = roleList;
    }}

18. 領域服務:

聚合根與領域服務負責封裝實現業務邏輯。領域服務負責對聚合根進行調度和封裝,同時可以對外提供各種形式的服務,對於不能直接通過聚合根完成的業務操作就需要通過領域服務。

說白了就是,聚合根本身無法完全處理這個邏輯,例如支付這個步驟,訂單聚合不可能支付,所以在訂單聚合上架一層領域服務,在領域服務中實現支付邏輯,然後應用服務調用領域服務。

在以下幾種情況時,我們可以使用領域服務:

  1. 對於不能直接通過聚合根完成的業務操作就需要通過領域服務。

  2. 在 DDD 中,每個實體只能操作自己實體的變化,不能改另一個實體的狀態。跨實體的狀態變化需要抽象出一個領域服務,不能直接修改實體的狀態,只能調用實體的業務方法。

  3. 以多個領域對象作爲輸入參數進行計算,結果產生一個值對象。

  4. 執行一個顯著的業務操作

  5. 對領域對象進行轉換

遵守以下規範

  1. 同限界上下文內的聚合之間的領域服務可直接調用

  2. 兩個限界上下文的交互必須通過應用服務層抽離接口 -> 適配層適配。

e.g. 用戶升職,上級領導要變,上級領導的下屬要變

代碼如下

/**
 * @Author WDYin
 * @Date 2022/5/15
 **/@Servicepublic class UserDomainServiceImpl implements UserDomainService {

    @Override
    public void promote(User user, User leader) {

        //保存領導
        user.saveLeader(leader);

        //領導增加下屬
        leader.increaseSubordination(user);
    }}

19. 應用服務:

應用服務作爲總體協調者,先通過資源庫獲取到聚合根,然後調用聚合根或者領域服務中的業務方法,最後再次調用資源庫保存聚合根。

作用:

  1. 除了同步方法調用外,還可以發佈或者訂閱領域事件,權限校驗、事務控制,一個事務對應一個聚合根。

  2. 應用層方法主要執行服務編排等輕量級邏輯,尤其針對跨多個領域的業務場景,效果明顯。

  3. 參數校驗,簡單的 crud,可直接調用倉庫接口

/**
 * @Author WDYin
 **/@Servicepublic class UserApplicationServiceImpl implements UserApplicationService {

    @Resource
    private DomainEventPublisher domainEventPublisher;

    @Resource
    private UserRepository userRepository;

    @Resource
    private UserAssembler userAssembler;

    @Resource
    private UserDomainService userDomainService;

        /**
     * 用戶註冊
     * @param userAddCommand 註冊信息
     */
    @Override
    public void register(UserAddCommand userAddCommand) {

        //業務檢查
        userDomainService.check(userAddCommand);

        //組裝user領域模型
        User user = userAssembler.commandToDo(userAddCommand);

        //落庫
        userRepository.add(user);

        //發送用戶註冊事件
        domainEventPublisher.publish(new UserRegisterEvent(user, UserEventTypeEnum.REGISTER));

    }

    /**
     * 用戶修改
     * @param userUpdateCommand 修改信息
     */
    @Override
    public void update(UserUpdateCommand userUpdateCommand) {
        User user = userRepository.getById(userUpdateCommand.getId());
        //發送用戶註冊事件
        domainEventPublisher.publish(new UserRegisterEvent(user, UserEventTypeEnum.CREATED));
    }

    /**
     * 用戶升職
     * @param userId 用戶id
     * @param leaderId 領導的id
     */
    @Override
    public void promote(UserId userId, UserId leaderId) {
    	//獲取用戶領域模型
        User user = userRepository.getById(userId.getUserId());
        //獲取用戶的新領導
        User leader = userRepository.getById(leaderId.getUserId());
        //領域服務升職方法
        userDomainService.promote(user,leader);
    }}

20. 倉庫:

負責提供聚合根或者持久化聚合根。倉庫幫助我們持久化整個聚合的,存一個對象會把相關對象都存下來。從技術上講,Repository 和 DAO 所扮演的角色相似,不過 DAO 的設計初衷只是對數據庫的一層很薄的封裝,而 Repository 是更偏向於領域模型。另外,在所有的領域對象中,只有聚合根才 “配得上” 擁有 Repository,而 DAO 沒有這種約束。

代碼如下

/**
 * @Author WDYin
 * @Date 2022/5/15
 **/@Repositorypublic class UserRepositoryImpl implements UserRepository {

    @Resource
    private UserPersistence userPersistence;

    @Resource
    private UserConverter userConverter;

    @Override
    public void add(User user) {
        UserPO userPo = userConverter.doToPo(user);
        userPersistence.insert(userPo);
        user.setId(userPo.getId());
    }

    @Override
    public User getById(Long id) {
        UserPO userPO = userPersistence.selectByPrimaryKey(id);
        return userConverter.poToDo(userPO);
    }}

21. 工廠:

比如說創建一個實體,裏面有五個值對象組成,每次創建的時候都得 new 一次,這裏用工廠簡化,工廠幫助我們創建聚合。這一方面可以享受到工廠模式本身的好處,另一方面,DDD 中的 Factory 還具有將 “聚合根的創建邏輯” 顯現出來的效果。Factory 有兩種實現方式:

  1. 直接在聚合根中實現 Factory 方法,常用於簡單的創建過程
  2. 獨立的 Factory 類,用於有一定複雜度的創建過程,或者創建邏輯不適合放在聚合根上
    工廠也可以使用 converter 來代替
    代碼如下
    屬性的拷貝使用 BeanUtils 或者 mapstruct 都可以。
@Componentpublic class UserConverter {

    public User poToDo(UserPO userPO) {
        User user = new User();
        BeanUtils.copyProperties(userPO,user);
        return user;
    }

    public UserPO doToPo(User user) {
        UserPo userPO = new UserPo();
        BeanUtils.copyProperties(userPO,user);
        return userPO;
    }}

22. 防腐層:

當某個功能模塊需要依賴第三方系統提供的數據或者功能時,我們常用的策略就是直接使用外部系統的 API、數據結構。這樣存在的問題就是,因使用外部系統,而被外部系統的質量問題影響,從而 “腐化” 本身設計的問題。

因此我們的解決方案就是在兩個系統之間加入一箇中間層,隔離第三方系統的依賴,對第三方系統進行通訊轉換和語義隔離,這個中間層,我們叫它防腐層。

說白了就是,兩個系統之間加了中間層,中間層類似適配器模式,解決接口差異的對接,接口轉換是單向的(即從調用方向被調用方進行接口轉換);防腐層強調兩個子系統語義解耦,接口轉換是雙向的。

防腐層作用:

  1. 使兩方的系統解耦,隔離雙方變更的影響,允許雙方獨立演進;

  2. 防腐層允許其它的外部系統能夠在不改變現有系統的領域層的前提下,與該系統實現無縫集成,從而降低系統集成的開發工作量。

2.4. 落地方式

DDD 分爲戰略設計和戰術設計。

2.4.1. 戰略設計:

參與人員:業務專家(領域專家),產品經理,技術專家(研發人員)
戰略設計更偏向於軟件架構的層面,官方解釋,在某個領域,核心圍繞上下文的設計。講求的是領域和限界上下文 (Bounded Context,BC) 的劃分,以及各個限界上下文之間的上下游關係,還有通用語言的設計。也就是從業務視角出發,歸好類,把邊界劃分好,明確界限上下文,可以用事件風暴來做。會得到通用語言,上下文,上下文之間的交互關係,邊界,不同的域。
說白了就是,在某個系統,核心圍繞子系統的設計;主要關注,這些子系統的劃分,子系統的交互方式,還有子系統的核心術語的定義。當前如此火熱的 “在微服務中使用 DDD” 這個命題,究其最初的邏輯無外乎是“DDD 中的限界上下文可以用於指導微服務中的服務劃分”,

事實上,限界上下文依然是軟件模塊化的一種體現。

三步走:
第一步:需求分析,根據需求劃分出初步的領域和限界上下文,以及上下文之間的關係;
第二步:領域分析,進一步分析每個上下文內部,抽取每個子域的領域概念,識別出哪些是實體,哪些是值對象;
第三步:領域建模,對實體、值對象進行關聯和聚合,劃分出聚合的範疇和聚合根;

2.4.2. 戰術設計:

參與人員:技術專家(研發人員)
戰術設計便更偏向於編碼實現,官方解釋,核心關注上下文中的實體建模,定義值對象,實體等,更偏向開發細節。用領域模型指導設計及編碼的實現,以技術爲主導。
說白了就是,上下文對應的就是某一個子系統,子系統裏代碼實現怎麼設計,就是戰術設計要解決的問題。核心關注某個子系統的代碼實現,以面向對象的思維設計類的屬性和方法,和設計類圖沒有什麼區別,只是有一些規則而已,就是指導我們劃分類。DDD 戰術設計的目的是使得業務能夠從技術中分離並突顯出來,讓代碼直接表達業務的本身。
其中包含了實體,聚合,聚合根,值對象,聚合之間的關係,倉庫,工廠,防腐層,充血模型,領域服務,領域事件等概念。
戰術層面可以說 DDD 是一種放大的設計模式。

三步走:
第一步:編寫核心業務邏輯,由領域模型驅動軟件設計,通過代碼來表現該領域模型,在實體和領域服務中實現核心業務邏輯;
第二步:爲聚合根設計倉儲,並思考實體或值對象的創建方式;
第三步:在工程中實踐領域模型,並在實踐中檢驗模型的合理性,倒推模型中不足的地方並重構。

3、DDD 架構

分層架構的一個重要原則是每層只能與位於其下方的層發生耦合,較低層絕不能直接訪問較高層。分層架構可以簡單分爲兩種:
嚴格分層架構:
某層只能與位於其直接下方的層發生耦合
鬆散分層架構:
則允許某層與它的任意下方層發生耦合
我們在實際運用過程中多使用的是鬆散分層架構。

3.1. 傳統四層架構:

將領域模型和業務邏輯分離出來,並減少對基礎設施、用戶界面甚至應用層邏輯的依賴,因爲它們不屬業務邏輯。將一個夏雜的系統分爲不同的層,每層都應該具有良好的內聚性,並且只依賴於比其自身更低的層。

傳統分層架構的 基礎設施層 位於底層,持久化和消息機制便位於該層。可將基礎設施層中所有組件看作應用程序的低層服務,較高層與該層發生耦合以複用技術基礎設施。即便如此,依然應避免核心的領域模型對象與基礎設施層直接耦合。

3.2. 改良版四層架構

傳統架構的缺陷:就是將基礎設施層放在最底層存在缺點,比如此時領域層中的一些技術實現令人頭疼:違背分層架構的基本原則,難以編寫測試用例等。

因此通過 Java 設計六大原則中的依賴倒置原則實現各層對基礎資源的解耦:也就是低層服務(如基礎設施層)應依賴高層組件(比如用戶界面層、應用層和領域層)所提供接口。高層定義好倉庫的接口,基礎設施層實現各層定義好的倉庫接口。
依賴倒置原則:具體依賴於抽象,而不是抽象依賴於具體。

(1)用戶接口層:

①一般包括用戶接口、Web 服務、rpc 請求,mq 消息等外部輸入均被視爲外部輸入的請求。對外暴露 API,具體形式不限於 RPC、Rest API、消息等。
②一般都很薄,提供必要的參數校驗和異常捕獲流程。
③一般會提供 VO 或者 DTO 到 Entity 或者 ValueObject 的轉換,用於前後端調用的適配,當然 dto 可以直接使用 command 和 query,視情況而定。
④用戶接口層很重要,在於前後端調用的適配。若你的微服務要面向很多應用或渠道提供服務,而每個渠道的入參出參都不一樣,你不太可能開發出太多應用服務,這樣 Facade 接口就起很好的作用了,包括 DO 和 DTO 對象的組裝和轉換等。

(2)應用層:

①應用層方法提供用例級別的能力透出,不處理業務邏輯,而只是調用領域層,對領域服務 / 聚合根方法調用的封裝,負責領域的組合、編排、轉發、轉換和傳遞。
②應用服務作爲總體協調者,先通過資源庫獲取到聚合根,然後調用聚合根中的業務方法,最後再次調用資源庫保存聚合根。
③除了同步方法調用外,還可以發佈或者訂閱領域事件,權限校驗、事務控制,一個事務對應一個聚合根。
④應用層方法主要執行服務編排等輕量級邏輯,尤其針對跨多個領域的業務場景,效果明顯。
⑤參數校驗,簡單的 crud,可直接調用倉庫接口
⑥跨上下文(kafka,RocketMq)和上下文內(spring 事件,Guava Event Bus)的領域事件
⑦倉儲層接口

(3)領域層:

①包含了業務核心的領域模型:實體(聚合根 + 值對象),使用充血模型實現所有與之相關的業務功能,主要表達業務概念,業務狀態信息以及業務規則。
②真正的業務邏輯都在領域層編寫,聚合根負責封裝實現業務邏輯,對應用層暴露領域級別的服務接口。
③聚合根不能直接操作其它聚合根,聚合根與聚合根之間只能通過聚合根 ID 引用;同限界上下文內的聚合之間的領域服務可直接調用;兩個限界上下文的交互必須通過應用服務層抽離接口 -> 適配層適配。
④跨實體的狀態變化,使用領域服務,領域服務不能直接修改實體的狀態,只能調用實體的業務方法
⑤在所有的領域對象中,只有聚合根才擁有 Repository,因爲 Repository 不同於 DAO,它所扮演的角色只是向領域模型提供聚合根。
⑥防腐層接口
⑦倉儲層接口

(4)基礎設施層:

①爲業務邏輯提供支撐能力,提供通用的技術能力,倉庫寫增刪改查類似 DAO。
② 防腐層實現 (封裝變化) 用於業務檢查和隔離第三方服務,內部 try catch
③ 聚合根工廠負責創建聚合根,但並非必須的,可以將聚合根的創建寫到聚合根下並改爲靜態方法。工廠組組裝複雜對象,可能會調用第三方服務,倉庫集成工廠 Facotry/build 應對複雜對象的封裝,也可以使用 converter。
④ 多於技術有關,如: DB 交互的接口、Cache 相關、MQ、工具類等
⑤抽象系統內第三方組件的交互,爲上層提供技術層面的支持,與業務細節無關。

3.3. 整潔架構 (洋蔥架構)

在整潔架構裏,同心圓代表應用軟件的不同部分,從裏到外依次是領域模型、領域服務、應用服務和最外圍的容易變化的內容,比如用戶界面和基礎設施。
整潔架構最主要的原則是依賴原則,它定義了各層的依賴關係,越往裏依賴越低,代碼級別越高,越是核心能力。外圓代碼依賴只能指向內圓,內圓不需要知道外圓的任何情況。

在洋蔥架構中,各層的職能劃分:

領域模型實現領域內核心業務邏輯,它封裝了企業級的業務規則。領域模型的主體是實體,一個實體可以是一個帶方法的對象,也可以是一個數據結構和方法集合。
領域服務實現涉及多個實體的複雜業務邏輯。應用服務實現與用戶操作相關的服務組合與編排,它包含了應用特有的業務流程規則,封裝和實現了系統所有用例。
最外層主要提供適配的能力,適配能力分爲主動適配和被動適配。主動適配主要實現外部用戶、網頁、批處理和自動化測試等對內層業務邏輯訪問適配。被動適配主要是實現核心業務邏輯對基礎資源訪問的適配,比如數據庫、緩存、文件系統和消息中間件等。
紅圈內的領域模型、領域服務和應用服務一起組成軟件核心業務能力。

3.4. CQRS 架構 (命令查詢隔離架構)

CQRS — Command Query Responsibility Segregation,故名思義是讀寫分離,就是將 command 與 query 分離的一種模式。
Command :命令則是對會引起數據發生變化操作的總稱,即我們常說的新增,更新,刪除這些操作,都是命令。
Query:查詢則和字面意思一樣,即不會對數據產生變化的操作,只是按照某些條件查找數據。
Command 與 Query 對應的數據源可以公用一種數據源,也可以是互相獨立的,即更新操作在一個數據源,而查詢操作在另一個數據源上。

CQRS 三種模式
(1)共享模型 / 共享存儲:讀寫公用一種領域模型,讀寫模型公用一種。
(2)分離模型 / 共享存儲:讀寫分別用不同的領域模型,讀操作使用讀領域模型,寫操作使用寫領域模型。
(3)分離模式 / 分離存儲:也叫做事件源 (Event source) CQRS,使用領域事件保證讀寫數據的一致性。也就是當 command 系統完成數據更新的操作後,會通過領域事件的方式通知 query 系統。query 系統在接受到事件之後更新自己的數據源。
CQRS(讀寫操作分別使用不同的數據庫)


軟件中的讀模型和寫模型是很不一樣的,我們通常所講的業務邏輯更多的時候是在寫操作過程中需要關注的東西,而讀操作更多關注的是如何向客戶方返回恰當的數據展現。

因此在 DDD 的寫操作中,我們需要嚴格地按照 “應用服務 -> 聚合根 -> 資源庫” 的結構進行編碼,而在讀操作中,採用與寫操作相同的結構有時不但得不到好處,反而使整個過程變得冗繁,還多了模型轉換,影響效率。本來讀操作就需要速度快,性能高。
因此本文 CQRS 實戰中的讀操作是基於數據模型,應用層提供一個單獨的用於讀的倉庫,然後繞過聚合根和資源庫,也就是繞過領域層,在應用層直接返回數據。而寫操作是基於領域模型,通過應用服務 -> 聚合根 / 領域服務 -> 資源庫的代碼結構進行編碼。

3.5. 六邊形架構 (端口適配器架構)

六邊形架構的核心理念是:應用是通過端口與外部進行交互的

下圖的紅圈內的核心業務邏輯(應用程序和領域模型)與外部資源(包括 APP、Web 應用以及數據庫資源等)完全隔離,僅通過適配器進行交互。它解決了業務邏輯與用戶界面的代碼交錯問題,很好地實現了前後端分離。六邊形架構各層的依賴關係與整潔架構一樣,都是由外向內依賴。


六邊形架構將系統分爲內六邊形和外六邊形兩層,這兩層的職能劃分如下:紅圈內的六邊形實現應用的核心業務邏輯;外六邊形完成外部應用、驅動和基礎資源等的交互和訪問,對前端應用以 API 主動適配的方式提供服務,對基礎資源以依賴倒置被動適配的方式實現資源訪問。六邊形架構的一個端口可能對應多個外部系統,不同的外部系統也可能會使用不同的適配器,由適配器負責協議轉換。這就使得應用程序能夠以一致的方式被用戶、程序、自動化測試和批處理腳本使用。

3.6. 總結


這三種架構模型的設計思想微服務架構高內聚低耦合原則的完美體現,而它們身上閃耀的正是以領域模型爲中心的設計思想,將核心業務邏輯與外部應用、基礎資源進行隔離。

紅色框內部主要實現核心業務邏輯,但核心業務邏輯也是有差異的,有的業務邏輯屬於領域模型的能力,有的則屬於面向用戶的用例和流程編排能力。按照這種功能的差異,我們在這三種架構中劃分了應用層和領域層,來承擔不同的業務邏輯。
領域層實現面向領域模型,實現領域模型的核心業務邏輯,屬於原子模型,它需要保持領域模型和業務邏輯的穩定,對外提供穩定的細粒度的領域服務,所以它處於架構的核心位置。
應用層實現面向用戶操作相關的用例和流程,對外提供粗粒度的 API 服務。它就像一個齒輪一樣進行前臺應用和領域層的適配,接收前臺需求,隨時做出響應和調整,儘量避免將前臺需求傳導到領域層。應用層作爲配速齒輪則位於前臺應用和領域層之間。

4、 CQRS 實戰

4.1. 概念

CQRS(Command Query Responsibility Segregation) 是將 Command(命令) 與 Query(查詢) 分離的一種模式。其基本思想在於:任何一個方法都可以拆分爲命令和查詢兩部分:

Command:不返回任何結果 (void),但會改變對象的狀態。Command 是引起數據變化操作的總稱,一般會執行某個動作,如:新增,更新,刪除等操作。操作都封裝在 Command 中,用戶提交 Commond 到 CommandBus,然後分發到對應的 CommandHandler 中執行。Command 執行後通過 Repository 將數據持久化。事件源 (Event source)CQRS,Command 將特定的 Event 發送到 EventBus,然後由特定的 EventHandler 處理。

Query:返回查詢結果,不會對數據產生變化的操作,只是按照某些條件查找數據。基於 Query 條件,返回查詢結果;爲不同的場景定製不同的 Facade。

4.2. 架構圖

基於四層的 CQRS 架構圖

4.3. 代碼佈局

第一種是

  1. 用戶界面層調用應用服務

  2. 應用服務調用領域服務

  3. 在領域服務中
    ①通過倉庫獲取聚合根
    ②通過資源庫持久化聚合根
    ③調用聚合根的業務方法
    ④發佈領域事件

第二種是:

  1. 用戶界面層調用應用服務

  2. 應用服務
    ①通過資源庫獲取聚合根
    ②調用聚合根的業務方法或者領域服務的方法
    ③通過資源庫持久化聚合根
    ④發佈領域事件

4.4. 數據模型轉換

每一層都有自己特定的數據,可以做如下區分:

  1. VO(View Object):視圖對象,主要對應界面顯示的數據對象。對於一個 WEB 頁面,小程序,微信公衆號等前端需要的數據對象。也有團隊用 VO 表示領域層中的 Value Object 值對象,這個要根據團隊的規範來定義。

  2. DTO(Data Transfer Object):數據傳輸對象,主要用於遠程調用之間傳輸的對象的地方。比如我們一張表有 100 個字段,那麼對應的 PO 就有 100 個屬性。但是客戶端只需要 10 個字段,沒有必要把整個 PO 對象傳遞到客戶端,這時我們就可以用只有這 10 個屬性的 DTO 來傳遞結果到客戶端,這樣也不會暴露服務端表結構。到達客戶端以後,如果用這個對象來對應界面顯示,那此時它的身份就轉爲 VO。DTO 泛指用於展示層與服務層之間的數據傳輸對象,當然 VO 也相當於數據 DTO 的一種。

  3. DO(Domain Object):領域對象,就是從現實世界中抽象出來的有形或無形的業務實體,使用的是充血模型設計的對象。也有團隊使用用 BO(Business Objects)表示業務對象的概念。

  4. PO(Persistent Object):持久化對象,它跟持久層(通常是關係型數據庫)的數據結構形成一一對應的映射關係,如果持久層是關係型數據庫,那麼,數據表中的每個字段(或若干個)就對應 PO 的一個(或若干個)屬性。最形象的理解就是一個 PO 就是數據庫中的一條記錄,好處是可以把一條記錄作爲一個對象處理,可以方便的轉爲其它對象。也有團隊使用 DO(Data Object)表示數據對象

  5. POJO(Plain Ordinary Java Object):簡單對象,是隻具有 setter getter 方法對象的統稱。但是不要把對象名命名成 xxxPOJO!

模型轉換架構圖

(1)從應用層 -> 基礎設施層的過程:

(2)從基礎設施層 -> 應用層的過程:

4.5. 項目目錄結構

│
│    ├─interface   用戶接口層 
│    │    └─controller    控制器,對外提供(Restful)接口
│    │    └─facade		  外觀模式,對外提供本地接口和dubbo接口
│    │    └─mq		      mq消息,消費者消費外部mq消息
│    │ 
│    ├─application 應用層
│    │    ├─assembler     裝配器
│    │    ├─dto           數據傳輸對象,xxxCommand/xxxQuery/xxxVo     
│    │    │    ├─command  接受增刪改的參數
│    │    │    ├─query    接受查詢的參數
│    │    │    ├─vo       返回給前端的vo對象
│    │    ├─service       應用服務,負責領域的組合、編排、轉發、轉換和傳遞
│    │    ├─repository    查詢數據的倉庫接口
│    │    ├─listener      事件監聽定義
│    │ 
│    ├─domain      領域層
│    │    ├─entity        領域實體
│    │    ├─valueobject   領域值對象
│    │    ├─service       領域服務
│    │    ├─repository    倉庫接口,增刪改的接口
│    │    ├─acl  		  防腐層接口
│    │    ├─event         領域事件
│    │ 
│    ├─infrastructure  基礎設施層
│    │    ├─converter     實體轉換器
│    │    ├─repository    倉庫
│    │    │    ├─impl     倉庫實現
│    │    │    ├─mapper   mybatis mapper接口
│    │    │    ├─po       數據庫orm數據對象 
│    │    ├─ack			  實體轉換器
│    │    ├─mq            mq消息
│    │    ├─cache         緩存
│    │    ├─util          工具類
│    │    
│

5、總結

  1. MVC 是一個短暫的快樂但不足以支撐漫長的生活,而 DDD 是一個不要短暫的溫存而是一世的陪伴,如果是你來抉擇你會選擇哪一個

  2. MVC 的開發模式:是數據驅動,自低向上的思想,關注數據。DDD 的開發模式:是領域驅動,自頂向下,關注業務活動。爲了應對業務快速變化的軟件系統,DDD 是面向對象的最終體現,大家一起用起來吧!

  3. DDD 是一套方法論,一千個讀者一千個哈姆雷特。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/QjaPk9YkfVFmmdVDKuyj2Q