領(lǐng)域驅(qū)動設(shè)計DDD和CQRS落地

image

前言

這篇文章假設(shè)你已經(jīng)初步了解過領(lǐng)域驅(qū)動設(shè)計(DDD)的基本概念(聚合根、實體秉沼、值對象桶雀、領(lǐng)域服務(wù)、領(lǐng)域事件唬复、資源庫背犯、限界上下文等)以及CQRS的設(shè)計,本文會將重點放在如何落地DDD和CQRS上盅抚。

DDD分層架構(gòu)

Evans在它的《領(lǐng)域驅(qū)動設(shè)計:軟件核心復(fù)雜性應(yīng)對之道》書中推薦采用分層架構(gòu)去實現(xiàn)領(lǐng)域驅(qū)動設(shè)計:

image

其實這種分層架構(gòu)我們早已駕輕就熟漠魏,MVC模式就是我們所熟知的一種分層架構(gòu),我們盡可能去設(shè)計每一層妄均,使其保持高度內(nèi)聚性柱锹,讓它們只對下層進行依賴,體現(xiàn)了高內(nèi)聚低耦合的思想丰包。

分層架構(gòu)的落地就簡單明了了禁熏,用戶界面層我們可以理解成web層的Controller,應(yīng)用層和業(yè)務(wù)無關(guān)邑彪,它負(fù)責(zé)協(xié)調(diào)領(lǐng)域?qū)舆M行工作瞧毙,領(lǐng)域?qū)邮穷I(lǐng)域驅(qū)動設(shè)計的業(yè)務(wù)核心,包含領(lǐng)域模型和領(lǐng)域服務(wù)寄症,領(lǐng)域?qū)拥闹攸c放在如何表達(dá)領(lǐng)域模型上宙彪,無需考慮顯示和存儲問題,基礎(chǔ)實施層是最底層有巧,提供基礎(chǔ)的接口和實現(xiàn)释漆,領(lǐng)域?qū)雍蛻?yīng)用服務(wù)層通過基礎(chǔ)實施層提供的接口實現(xiàn)類如持久化、發(fā)送消息等功能篮迎。阿里巴巴開源的整潔面向?qū)ο蚍謱蛹軜?gòu)COLA就采取了這樣的分層架構(gòu)來實現(xiàn)領(lǐng)域驅(qū)動男图。

改進DDD分層架構(gòu)和DIP依賴倒置原則

DDD分層架構(gòu)是一種可落地的架構(gòu),但是我們依然可以進行改進甜橱,Vernon在它的《實現(xiàn)領(lǐng)域驅(qū)動設(shè)計》一書中提到了采用依賴倒置原則改進的方案逊笆。

所謂的依賴倒置原則指的是:高層模塊不應(yīng)該依賴于低層模塊,兩者都應(yīng)該依賴于抽象岂傲,抽象不應(yīng)該依賴于細(xì)節(jié)难裆,細(xì)節(jié)應(yīng)該依賴于抽象。

image

從圖中可以看到譬胎,基礎(chǔ)實施層位于其他所有層的上方差牛,接口定義在其它層命锄,基礎(chǔ)實施實現(xiàn)這些接口。依賴原則的定義在DDD設(shè)計中可以改述為:領(lǐng)域?qū)拥绕渌麑硬粦?yīng)該依賴于基礎(chǔ)實施層偏化,兩者都應(yīng)該依賴于抽象脐恩,具體落地的時候,這些抽象的接口定義放在了領(lǐng)域?qū)拥认路綄又姓焯帧_@也就是意味著一個重要的落地指導(dǎo)原則: 所有依賴基礎(chǔ)實施實現(xiàn)的抽象接口驶冒,都應(yīng)該定義在領(lǐng)域?qū)踊驊?yīng)用層中

采用依賴倒置原則改進DDD分層架構(gòu)除了上面說的DIP的好處外韵卤,還有什么好處嗎骗污?其實這種分層結(jié)構(gòu)更加地高內(nèi)聚低耦合。每一層只依賴于抽象沈条,因為具體的實現(xiàn)在基礎(chǔ)實施層需忿,無需關(guān)心。只要抽象不變蜡歹,就無需改動那一層屋厘,實現(xiàn)如果需要改變,只需要修改基礎(chǔ)實施層就可以了月而。

采用依賴倒置原則的代碼落地中汗洒,資源庫Repository的抽象接口定義就會放在領(lǐng)域?qū)恿耍挛臅訇U述如何落地Repository父款。

六邊形架構(gòu)溢谤、洋蔥架構(gòu)、整潔架構(gòu)

《實現(xiàn)領(lǐng)域驅(qū)動設(shè)計》一書中提到了DDD架構(gòu)更深層次的變化憨攒,Vernon放棄了分層架構(gòu)世杀,采用了對稱性架構(gòu):六邊形架構(gòu),作者認(rèn)為這是一種具有持久生命力的架構(gòu)浓恶。當(dāng)你真正理解這種架構(gòu)的時候玫坛,相信你也不得不佩服這種角度不同的設(shè)計。

image

如上圖包晰,在這種架構(gòu)風(fēng)格中,外部客戶和內(nèi)部系統(tǒng)的交互都會通過端口和適配器完成轉(zhuǎn)換炕吸,這些外部客戶之間是平等的伐憾,比如用戶web界面和數(shù)據(jù)庫持久化,當(dāng)你需要一個新的外部客戶時赫模,只需要增加相應(yīng)的適配器树肃,比如當(dāng)我們增加外部一個RPC的服務(wù)時,只需要編寫對應(yīng)的適配器即可瀑罗。

好吧胸嘴,當(dāng)將web界面和持久化統(tǒng)稱在一起雏掠,沒有前端和數(shù)據(jù)庫后端之分的時候,這種觀察架構(gòu)的角度已經(jīng)打動到了我劣像。

那么適配器在各種外部客戶的場景下時什么呢乡话?如果外部客戶時HTTP請求,那么SpringMVC的注解和Controller構(gòu)成了適配器耳奕,如果外部客戶時MQ消息绑青,那么適配器就是MQConsumer監(jiān)聽器,如果外部客戶時數(shù)據(jù)庫屋群,那么適配器可能就是Mybatis的Mapper闸婴。

隨著架構(gòu)的演化,后來又提出了洋蔥架構(gòu)和整潔架構(gòu)芍躏,這些架構(gòu)大同小異邪乍,它們都只允許外層依賴內(nèi)層,不允許內(nèi)層知道外層的細(xì)節(jié)对竣,下圖是整潔架構(gòu)圖溺欧,詳細(xì)介紹這里就不作贅述,可以參考這篇文章:The Clean Architecture柏肪。

image

SIDE-EFFECT-FREE模式和CQRS架構(gòu)落地

SIDE-EFFECT-FREE模式被稱為無副作用模式姐刁,熟悉函數(shù)時編程的朋友都知道,嚴(yán)格的函數(shù)就是一個無副作用的函數(shù)烦味,對于一個給定的輸入聂使,總是返回固定的結(jié)果,通常查詢功能就是一個函數(shù)谬俄,命令功能就不是一個函數(shù)柏靶,它通常會執(zhí)行某些修改。

在DDD架構(gòu)中溃论,通常會將查詢和命令操作分開屎蜓,我們稱之為CQRS(命令查詢的責(zé)任分離Command Query Responsibility Segregation),具體落地時钥勋,是否將Command和Query分開成兩個項目可以看情況決定炬转,大多數(shù)情況下放在一個項目可以提高業(yè)務(wù)內(nèi)聚性,下面這張圖是來自
Martin Fowler的文章:CQRS算灸。

image

這張圖讀寫只是邏輯分離扼劈,物理層面還是使用了一個數(shù)據(jù)庫,我們可以將數(shù)據(jù)庫改成讀庫和寫庫做到物理分離菲驴,這時候就需要同步都寫庫荐吵,業(yè)界的解決方案是當(dāng)寫庫發(fā)生更改時,通過Event事件機制通知讀庫進行同步。

最終CQRS落地的方案我們選擇了簡單化處理先煎,物理層面還是使用一個數(shù)據(jù)庫贼涩,查詢的時候部分?jǐn)?shù)據(jù)直接從數(shù)據(jù)庫讀取,部分?jǐn)?shù)據(jù)使用到了Elasticsearch薯蝎,當(dāng)數(shù)據(jù)庫發(fā)生更改時遥倦,會發(fā)送Event事件通知ES進行更新。當(dāng)然我們還可以更加技術(shù)的處理這種同步良风,我們可以去除事件谊迄,直接監(jiān)聽Mysql的binlog更新ES,而我們也正是這樣做的烟央。

DDD统诺、CQRS架構(gòu)落地

根據(jù)上面的分析,最終落地的DDD+CQRS的架構(gòu)使用了對稱性架構(gòu)疑俭,如下圖所示:

image

架構(gòu)中粮呢,我們平等的看待Web、RPC钞艇、DB啄寡、MQ等外部服務(wù),基礎(chǔ)實施依賴圓圈內(nèi)部的抽象哩照。

當(dāng)一個命令Command請求過來時挺物,會通過應(yīng)用層的CommandService去協(xié)調(diào)領(lǐng)域?qū)庸ぷ鳎粋€查詢Query請求過來時飘弧,則直接通過基礎(chǔ)實施的實現(xiàn)與數(shù)據(jù)庫或者外部服務(wù)交互识藤。再次強調(diào),我們所有的抽象都定義在圓圈內(nèi)部次伶,實現(xiàn)都在基礎(chǔ)設(shè)施痴昧。

在具體落地中我們發(fā)現(xiàn),Query和Command的有一些數(shù)據(jù)和抽象服務(wù)是公用的冠王,因此我們抽出了一個新的模塊:Shared Data & Service赶撰,這個模塊的功能為公用的數(shù)據(jù)對象和抽象接口。

DDD柱彻、CQRS代碼落地

分析DDD架構(gòu)的方法論有很多豪娜,但是落地到代碼層面的方法論少之又少,這一小節(jié)我們將具體到DDD設(shè)計的每個小點來闡述如何代碼落地绒疗,下圖中代碼模塊的組織正好對應(yīng)了架構(gòu)的設(shè)計侵歇。

image

Web放在了模塊com.deepoove.cargo.web.controller中,實現(xiàn)一些Controller吓蘑,infrastructure放在了com.deepoove.cargo.infrastructure中,抽象接口的實現(xiàn)。它們都依賴于應(yīng)用服務(wù)和領(lǐng)域模型磨镶。

落地用戶界面com.deepoove.cargo.web.controller

Controller作為六邊形架構(gòu)中與HTTP端口的適配器溃蔫,起到了適配請求,委托應(yīng)用服務(wù)處理的任務(wù)琳猫。對稱性架構(gòu)的好處就在于伟叛,當(dāng)增加新的用戶的界面時我們可以創(chuàng)建一個新包去承載適配器(比如為暴露RPC服務(wù)創(chuàng)建com.deepoove.cargo.remoting包),然后調(diào)用應(yīng)用層的服務(wù)脐嫂。這里我們有一個規(guī)范:所有查詢的條件封裝成XXXQry對象统刮,所有命令的請求封裝成XXXCommand對象。

package com.deepoove.cargo.web.controller;

@RestController
@RequestMapping("/cargo")
public class CargoController {

    @Autowired
    CargoQueryService cargoQueryService;
    @Autowired
    CargoCmdService cargoCmdService;


    @RequestMapping(value = "/{cargoId}", method = RequestMethod.GET)
    public CargoDTO cargo(@PathVariable String cargoId) {
        return cargoQueryService.getCargo(cargoId);
    }

    @RequestMapping(method = RequestMethod.POST)
    public void book(@RequestBody CargoBookCommand cargoBookCommand) {
        cargoCmdService.bookCargo(cargoBookCommand);
    }

    @RequestMapping(value = "/{cargoId}/delivery", method = RequestMethod.PUT)
    public void modifydestinationLocationCode(@PathVariable String cargoId,
            @RequestBody CargoDeliveryUpdateCommand cmd) {
        cmd.setCargoId(cargoId);
        cargoCmdService.updateCargoDelivery(cmd);
    }

}

我們考慮校驗邏輯應(yīng)該放到哪一層的時候確定這一層代碼可以有請求參數(shù)的基本校驗账千,但是 應(yīng)用服務(wù)的校驗邏輯是必須存在的侥蒙,校驗和應(yīng)用服務(wù)的耦合是緊密的

落地應(yīng)用服務(wù)com.deepoove.cargo.application.command

com.deepoove.cargo.application.command包里面是具體CommandService的抽象和實現(xiàn)匀奏,它將協(xié)調(diào)領(lǐng)域模型和領(lǐng)域服務(wù)完成業(yè)務(wù)功能鞭衩,此處不包含任何邏輯。我們認(rèn)為應(yīng)用服務(wù)的每個方法與用例是一一對應(yīng)的娃善,典型的處理流程是:

  1. 校驗
  2. 協(xié)調(diào)領(lǐng)域模型或者領(lǐng)域服務(wù)
  3. 持久化
  4. 發(fā)布領(lǐng)域事件

在這一層可以使用流程編排论衍,典型的流程也可以使用技術(shù)手段固化,比如抽象模板模式聚磺。

package com.deepoove.cargo.application.command.impl;

@Service
public class CargoCmdServiceImpl implements CargoCmdService {

    @Autowired
    private CargoRepository cargoRepository;
    @Autowired
    DomainEventPublisher domainEventPublisher;

    @Override
    public void bookCargo(CargoBookCommand cargoBookCommand) {
        // create Cargo
        DeliverySpecification delivery = new DeliverySpecification(
                cargoBookCommand.getOriginLocationCode(),
                cargoBookCommand.getDestinationLocationCode());

        Cargo cargo = Cargo.newCargo(CargoDomainService.nextCargoId(), cargoBookCommand.getSenderPhone(),
                cargoBookCommand.getDescription(), delivery);

        // saveCargo
        cargoRepository.save(cargo);
        
        // post domain event
        domainEventPublisher.publish(new CargoBookDomainEvent(cargo));
    }

    @Override
    public void updateCargoDelivery(CargoDeliveryUpdateCommand cmd) {
        // validate

        // find
        Cargo cargo = cargoRepository.find(cmd.getCargoId());

        // domain logic
        cargo.changeDelivery(cmd.getDestinationLocationCode());

        // save
        cargoRepository.save(cargo);
    }

}

我們再看看應(yīng)用服務(wù)的代碼發(fā)現(xiàn)坯台,發(fā)布領(lǐng)域事件的動作放在了應(yīng)用層沒有放在領(lǐng)域?qū)樱I(lǐng)域事件的定義是在領(lǐng)域?qū)?緊接著會提到)瘫寝,這是為什么呢蜒蕾?如果 不考慮持久化,發(fā)布領(lǐng)域事件的確應(yīng)該在領(lǐng)域模型中矢沿,但是在代碼落地時滥搭,考慮到持久化完成后才代表有了真實的事件,所以我們將觸發(fā)事件的代碼放到了資源庫后面捣鲸。

落地領(lǐng)域模型com.deepoove.cargo.domain.aggregate

我們采用了aggregate而不是model瑟匆,是為了將聚合根的概念顯現(xiàn)出來,每個聚合根單獨成一個子包栽惶,在單個聚合根中包含所需要的 值對象愁溜、領(lǐng)域事件的定義、資源庫的抽象接口等外厂,這里解釋下為什么這些對象會在領(lǐng)域模型中冕象,因為它們更能體現(xiàn)這個領(lǐng)域模型,而且資源庫的抽象和聚合根有著對應(yīng)的關(guān)系(不大于聚合根的數(shù)量)汁蝶。

package com.deepoove.cargo.domain.aggregate.cargo;

import com.deepoove.cargo.domain.aggregate.cargo.valueobject.DeliverySpecification;

public class Cargo {

    private String id;
    private String senderPhone;
    private String description;
    private DeliverySpecification delivery;

    public Cargo(String id) {
        this.id = id;
    }

    public Cargo() {}

    /**
     * Factory method:預(yù)訂新的貨物
     * 
     * @param senderPhone
     * @param description
     * @param delivery
     * @return
     */
    public static Cargo newCargo(String id, String senderPhone, String description,
            DeliverySpecification delivery) {
        Cargo cargo = new Cargo(id);
        cargo.senderPhone = senderPhone;
        cargo.description = description;
        cargo.delivery = delivery;
        return cargo;
    }


    public void changeDelivery(String destinationLocationCode) {
        if (this.delivery
                .getOriginLocationCode().equals(destinationLocationCode)) { throw new IllegalArgumentException(
                        "destination and origin location cannot be the same."); }
        this.delivery.setDestinationLocationCode(destinationLocationCode);
    }

    public void changeSender(String senderPhone) {
        this.senderPhone = senderPhone;
    }

}

特別提醒的是渐扮,聚合根對象的創(chuàng)建不應(yīng)該被Spring容器管理论悴,也不應(yīng)該被注入其它對象。我們注意到聚合根對象可以通過靜態(tài)工廠方法Factory Method來創(chuàng)建墓律,下文還會介紹如何落地資源庫Repository進行聚合根的創(chuàng)建膀估。

落地領(lǐng)域服務(wù)com.deepoove.cargo.domain.service

很多朋友無法判斷業(yè)務(wù)邏輯什么時候該放在領(lǐng)域模型中,什么時候放在領(lǐng)域服務(wù)中耻讽,可以從以下幾點考慮:

  1. 不是屬于單個聚合根的業(yè)務(wù)或者需要多個聚合根配合的業(yè)務(wù)察纯,放在領(lǐng)域服務(wù)中,注意是業(yè)務(wù)针肥,如果沒有業(yè)務(wù)饼记,協(xié)調(diào)工作應(yīng)該放到應(yīng)用服務(wù)中
  2. 靜態(tài)方法放在領(lǐng)域服務(wù)中
  3. 需要通過rpc等其它外部服務(wù)處理業(yè)務(wù)的,放在領(lǐng)域服務(wù)中
package com.deepoove.cargo.domain.service;

@Service
public class CargoDomainService {

    public static final int MAX_CARGO_LIMIT = 10;
    public static final String PREFIX_ID = "CARGO-NO-";

    /**
     * 貨物物流id生成規(guī)則
     * 
     * @return
     */
    public static String nextCargoId() {
        return PREFIX_ID + (10000 + new Random().nextInt(9999));
    }

    public void updateCargoSender(Cargo cargo, String senderPhone, HandlingEvent latestEvent) {

        if (null != latestEvent
                && !latestEvent.canModifyCargo()) { throw new IllegalArgumentException(
                        "Sender cannot be changed after RECIEVER Status."); }

        cargo.changeSender(senderPhone);
    }

}

落地基礎(chǔ)設(shè)施com.deepoove.cargo.infrastructure

基礎(chǔ)設(shè)施可以對抽象的接口進行實現(xiàn)慰枕,上文中說到資源庫Repository的接口定義在領(lǐng)域?qū)泳咴颍敲丛诨A(chǔ)設(shè)施中就可以具體實現(xiàn)這個接口。

package com.deepoove.cargo.infrastructure.db.repository;

@Component
public class CargoRepositoryImpl implements CargoRepository {

    @Autowired
    private CargoMapper cargoMapper;

    @Override
    public Cargo find(String id) {
        CargoDO cargoDO = cargoMapper.select(id);
        Cargo cargo = CargoConverter.deserialize(cargoDO);
        return cargo;
    }

    @Override
    public void save(Cargo cargo) {
        CargoDO cargoDO = CargoConverter.serialize(cargo);
        CargoDO data = cargoMapper.select(cargoDO.getId());
        if (null == data) {
            cargoMapper.save(cargoDO);
        } else {
            cargoMapper.update(cargoDO);
        }
    }

}

資源庫Repository的實現(xiàn)就是將聚合根對象持久化捺僻,往往聚合根的定義和數(shù)據(jù)庫中定義的結(jié)構(gòu)并不一致乡洼,我們將數(shù)據(jù)庫的對象稱為數(shù)據(jù)對象DO,當(dāng)持久化時就需要將聚合根 序列化 成數(shù)據(jù)庫數(shù)據(jù)對象匕坯,通過資源庫獲取(構(gòu)造)聚合根時束昵,也需要 反序列化 數(shù)據(jù)庫數(shù)據(jù)對象。

我們可以基于反射或其它技術(shù)手段完成序列化和反序列化操作葛峻,這樣可以避免聚合根中編寫過多的getter和setter方法锹雏。

落地查詢服務(wù)com.deepoove.cargo.application.query

application應(yīng)用服務(wù)包含了commond和query兩個子包,其實query可以提取出去和application平級术奖,但是這兩種做法沒有對錯礁遵,只是選擇問題。

CQRS中查詢服務(wù)不會調(diào)用應(yīng)用服務(wù)采记,也不會調(diào)用領(lǐng)域模型和資源庫Repository佣耐,它會直接查詢數(shù)據(jù)庫或者ES獲取原始數(shù)據(jù)對象DO,然后組裝成數(shù)據(jù)傳輸對象DTO給用戶界面唧龄,這個組裝的過程稱為Assembler兼砖,由于與用戶界面有一定的對應(yīng)關(guān)系,所以Assembler放在查詢服務(wù)中既棺。

那么問題來了讽挟,查詢服務(wù)中如何獲取DO呢?通常的做法是在查詢模塊中定義抽象接口丸冕,由基礎(chǔ)設(shè)施實現(xiàn)從數(shù)據(jù)庫獲取數(shù)據(jù) 耽梅,但是DO的定義不是在基礎(chǔ)設(shè)施層嗎,查詢服務(wù)怎么可以訪問到這些對象呢胖烛?我們有兩個辦法:

  1. 查詢服務(wù)中定義一套一摸一樣的DO眼姐,然后基礎(chǔ)設(shè)施做轉(zhuǎn)換诅迷,缺點是有點復(fù)雜,冗余了DO妥凳,優(yōu)點是完美符合DIP原則:抽象在查詢服務(wù)中竟贯,實現(xiàn)在基礎(chǔ)設(shè)施答捕。
  2. 將DO放到shared Data & Service中去逝钥,這樣就只要一套DO供查詢服務(wù)和命令服務(wù)使用,查詢服務(wù)定義接口拱镐,基礎(chǔ)設(shè)施實現(xiàn)接口

具體落地我們發(fā)現(xiàn)方法1太復(fù)雜了艘款,方法2和mybatis結(jié)合會產(chǎn)生疑惑,因為mybatis Mapper就是一個接口沃琅,何須在查詢服務(wù)中再定義一套接口呢哗咆?最終落地的代碼在查詢服務(wù)和DB交互時 破壞了DIP原則,直接依賴Mapper讀取數(shù)據(jù)對象進行組裝益眉。

我們來看看一個查詢服務(wù)的實現(xiàn)晌柬,其中CargoDTOAssembler是一個組裝器:

package com.deepoove.cargo.application.query.impl;

@Service
public class CargoQueryServiceImpl implements CargoQueryService {

    @Autowired
    private CargoMapper cargoMapper;
    
    @Autowired
    private CargoDTOAssembler converter;

    @Override
    public List<CargoDTO> queryCargos() {
        List<CargoDO> cargos = cargoMapper.selectAll();
        return cargos.stream().map(converter::apply).collect(Collectors.toList());
    }

    @Override
    public List<CargoDTO> queryCargos(CargoFindbyCustomerQry qry) {
        List<CargoDO> cargos = cargoMapper.selectByCustomer(qry.getCustomerPhone());
        return cargos.stream().map(converter::apply).collect(Collectors.toList());
    }

    @Override
    public CargoDTO getCargo(String cargoId) {
        CargoDO select = cargoMapper.select(cargoId);
        return converter.apply(select);
    }
}

是否需要將每個對象都轉(zhuǎn)化成DTO返回給用戶界面這個要看情況,個人認(rèn)為當(dāng)DO能滿足界面需求時是可以直接返回DO數(shù)據(jù)的郭脂。

落地MQ年碘、Event、Cache等

毫無疑問展鸡,MQ屿衅、Event、Cache的實現(xiàn)都應(yīng)該在基礎(chǔ)設(shè)施層莹弊,它們接口的定義放在哪里呢涤久?一個方案是哪一層使用了抽象就在那一層定義接口,另一個方案是放到一個共有的抽象包下忍弛,基礎(chǔ)設(shè)施和對應(yīng)層依賴這個共有的抽象包响迂。

最終落地我選擇將這些接口放在了com.deepoove.cargo.shared包下,這個包的定義就是共有的數(shù)據(jù)和抽象细疚。

我們以領(lǐng)域事件為例來看看代碼如何實現(xiàn)蔗彤,首先定義抽象接口com.deepoove.cargo.shared.DomainEventPublisher

package com.deepoove.cargo.shared;

public interface DomainEventPublisher {
    public void publish(Object event);
}

然后在基礎(chǔ)實施中實現(xiàn),具體實現(xiàn)采用guava的Eventbus方案:

package com.deepoove.cargo.infrastructure.event;

@Component
public class GuavaDomainEventPublisher implements DomainEventPublisher {

    @Autowired
    EventBus eventBus;

    public void publish(Object event) {
        eventBus.post(event);
    }

}

發(fā)送事件的代碼已經(jīng)落地惠昔,那么監(jiān)聽事件的代碼應(yīng)該如何落地了呢幕与?同樣的,監(jiān)聽MQ的代碼如何落地呢镇防?按照架構(gòu)圖的指導(dǎo)啦鸣,這些 監(jiān)聽器都應(yīng)該充當(dāng)著適配器的作用,所以它們的落地都應(yīng)該放在基礎(chǔ)設(shè)施層来氧。

我們來看看具體監(jiān)聽器的實現(xiàn):

package com.deepoove.cargo.infrastructure.event.comsumer;

@Component
public class CargoListener {
    
    @Autowired
    private CargoCmdService cargoCmdService;
    @Autowired
    private EventBus eventBus;
    
    @PostConstruct
    public void init(){
        eventBus.register(this);
    }

    @Subscribe
    public void recordCargoBook(CargoBookDomainEvent event) {
        // invoke application service or domain service
    }
}

監(jiān)聽器的基本流程就是適配領(lǐng)域事件诫给,然后調(diào)用應(yīng)用服務(wù)去處理香拉。

落地RPC和防腐層

前面提到過,當(dāng)我們暴露一個RPC服務(wù)時和web層是平等對待的中狂,比如暴露一個dubbo協(xié)議的服務(wù)就和暴露一個http的服務(wù)是平等的凫碌。這一小節(jié)我們將來探討如何與第三方系統(tǒng)的RPC服務(wù)進行交互。

這里涉及到DDD中Bounded Context和Context Map的概念胃榕,在領(lǐng)域驅(qū)動設(shè)計中盛险,限界上下文之間是不能直接交互的,它們需要通過Context Map進行交互勋又,在微服務(wù)足夠細(xì)致的年代苦掘,我們可以做到一個微服務(wù)就代表著一個限界上下文。

當(dāng)我們與第三方系統(tǒng)RPC交互時楔壤,就要考慮如何設(shè)計Context Map鹤啡,典型的模式有Shared Kernel共享內(nèi)核模式和Anti-corruption防腐層模式,最終落地時我們選擇了防腐層模式蹲嚣,它的結(jié)構(gòu)如下圖(圖來自《實現(xiàn)領(lǐng)域驅(qū)動設(shè)計》一書)所示:

image

圖中Adapter就是適配器递瑰,通用做法會再創(chuàng)建一個Translator實現(xiàn)上下文模型之間的翻譯功能。

在看具體代碼落地前還有一個問題需要強調(diào)隙畜,其它限界上下文的模型在我們系統(tǒng)中并不是一個模型實體抖部,而是一個值對象,很顯然Adapter應(yīng)該放在基礎(chǔ)設(shè)施層中禾蚕,那么這個值對象存放在哪里呢您朽?

我們可以將值對象和抽象接口定義在領(lǐng)域?qū)樱缓蠡A(chǔ)設(shè)施通過適配器和翻譯器實現(xiàn)抽象接口换淆,很明顯這個做法是非郴┳埽可取的。在具體落地時我們發(fā)現(xiàn)倍试,這些值對象可能同時又被查詢服務(wù)依賴讯屈,所以值對象和抽象接口定義在shared Data & Service中也是可取的,具體放在那里因看法而異县习。

接下來我們來看看適配器的基本實現(xiàn)涮母,其中RemoteServiceTranslator起到了模型之間翻譯的作用。

package com.deepoove.cargo.infrastructure.rpc.salessystem;
@Component
public class RemoteServiceAdapter {

    @Autowired
    private RemoteServiceTranslator translator;

    // @Autowired
    // remoteService

    public UserDO getUser(String phone) {
        // User user = remoteService.getUser(phone);
        // return this.translator.toUserDO(user);
        return null;
    }

    public EnterpriseSegment deriveEnterpriseSegment(Cargo cargo) {
        // remote service
        // translator
        return EnterpriseSegment.FRUIT;
    }

}

Cargo貨物實例和源碼

落地代碼實現(xiàn)了一個簡單的貨運系統(tǒng)躁愿,主要功能包括預(yù)訂貨物叛本、修改貨運信息、添加貨運事件和追蹤貨運物流信息等彤钟,具體源碼參見:GitHub:https://github.com/Sayi/ddd-cargo

image

參考資料

在整個落地過程中来候,每次閱讀《領(lǐng)域驅(qū)動設(shè)計》和《實現(xiàn)領(lǐng)域驅(qū)動設(shè)計》這兩本書都會給我?guī)硇碌南敕ǎ档猛扑]逸雹。
The Clean Architecture
DDD, Hexagonal, Onion, Clean, CQRS
dddsample-core

總結(jié)

所有的落地代碼都是當(dāng)前的想法营搅,它一定會變化云挟,架構(gòu)和設(shè)計有魅力的地方就在于它會不斷的變遷和升級,我們會不斷豐富在領(lǐng)域驅(qū)動設(shè)計中的代碼落地转质,也歡迎在評論中與我探討DDD相關(guān)的話題园欣。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市休蟹,隨后出現(xiàn)的幾起案子沸枯,更是在濱河造成了極大的恐慌,老刑警劉巖鸡挠,帶你破解...
    沈念sama閱讀 216,372評論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件辉饱,死亡現(xiàn)場離奇詭異,居然都是意外死亡拣展,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評論 3 392
  • 文/潘曉璐 我一進店門缔逛,熙熙樓的掌柜王于貴愁眉苦臉地迎上來备埃,“玉大人,你說我怎么就攤上這事褐奴“唇牛” “怎么了?”我有些...
    開封第一講書人閱讀 162,415評論 0 353
  • 文/不壞的土叔 我叫張陵敦冬,是天一觀的道長辅搬。 經(jīng)常有香客問我,道長脖旱,這世上最難降的妖魔是什么堪遂? 我笑而不...
    開封第一講書人閱讀 58,157評論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮萌庆,結(jié)果婚禮上溶褪,老公的妹妹穿的比我還像新娘。我一直安慰自己践险,他們只是感情好猿妈,可當(dāng)我...
    茶點故事閱讀 67,171評論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著巍虫,像睡著了一般彭则。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上占遥,一...
    開封第一講書人閱讀 51,125評論 1 297
  • 那天俯抖,我揣著相機與錄音,去河邊找鬼筷频。 笑死蚌成,一個胖子當(dāng)著我的面吹牛前痘,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播担忧,決...
    沈念sama閱讀 40,028評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼芹缔,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了瓶盛?” 一聲冷哼從身側(cè)響起最欠,我...
    開封第一講書人閱讀 38,887評論 0 274
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎惩猫,沒想到半個月后芝硬,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,310評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡轧房,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,533評論 2 332
  • 正文 我和宋清朗相戀三年拌阴,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片奶镶。...
    茶點故事閱讀 39,690評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡迟赃,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出厂镇,到底是詐尸還是另有隱情纤壁,我是刑警寧澤,帶...
    沈念sama閱讀 35,411評論 5 343
  • 正文 年R本政府宣布捺信,位于F島的核電站酌媒,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏迄靠。R本人自食惡果不足惜秒咨,卻給世界環(huán)境...
    茶點故事閱讀 41,004評論 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望梨水。 院中可真熱鬧拭荤,春花似錦、人聲如沸疫诽。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽奇徒。三九已至雏亚,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間摩钙,已是汗流浹背罢低。 一陣腳步聲響...
    開封第一講書人閱讀 32,812評論 1 268
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人网持。 一個月前我還...
    沈念sama閱讀 47,693評論 2 368
  • 正文 我出身青樓宜岛,卻偏偏與公主長得像,于是被迫代替她去往敵國和親功舀。 傳聞我的和親對象是個殘疾皇子萍倡,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,577評論 2 353

推薦閱讀更多精彩內(nèi)容