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

image

前言

這篇文章假設(shè)你已經(jīng)初步了解過(guò)領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)(DDD)的基本概念(聚合根、實(shí)體、值對(duì)象、領(lǐng)域服務(wù)、領(lǐng)域事件、資源庫(kù)、限界上下文等)以及CQRS的設(shè)計(jì),本文會(huì)將重點(diǎn)放在如何落地DDD和CQRS上。

DDD分層架構(gòu)

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

image

其實(shí)這種分層架構(gòu)我們?cè)缫疡{輕就熟,MVC模式就是我們所熟知的一種分層架構(gòu),我們盡可能去設(shè)計(jì)每一層,使其保持高度內(nèi)聚性,讓它們只對(duì)下層進(jìn)行依賴,體現(xiàn)了高內(nèi)聚低耦合的思想。

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

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

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

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

image

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

采用依賴倒置原則改進(jìn)DDD分層架構(gòu)除了上面說(shuō)的DIP的好處外,還有什么好處嗎?其實(shí)這種分層結(jié)構(gòu)更加地高內(nèi)聚低耦合。每一層只依賴于抽象,因?yàn)榫唧w的實(shí)現(xiàn)在基礎(chǔ)實(shí)施層,無(wú)需關(guān)心。只要抽象不變,就無(wú)需改動(dòng)那一層,實(shí)現(xiàn)如果需要改變,只需要修改基礎(chǔ)實(shí)施層就可以了。

采用依賴倒置原則的代碼落地中,資源庫(kù)Repository的抽象接口定義就會(huì)放在領(lǐng)域?qū)恿耍挛臅?huì)再闡述如何落地Repository。

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

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

image

如上圖,在這種架構(gòu)風(fēng)格中,外部客戶和內(nèi)部系統(tǒng)的交互都會(huì)通過(guò)端口和適配器完成轉(zhuǎn)換,這些外部客戶之間是平等的,比如用戶web界面和數(shù)據(jù)庫(kù)持久化,當(dāng)你需要一個(gè)新的外部客戶時(shí),只需要增加相應(yīng)的適配器,比如當(dāng)我們?cè)黾油獠恳粋€(gè)RPC的服務(wù)時(shí),只需要編寫對(duì)應(yīng)的適配器即可。

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

那么適配器在各種外部客戶的場(chǎng)景下時(shí)什么呢?如果外部客戶時(shí)HTTP請(qǐng)求,那么SpringMVC的注解和Controller構(gòu)成了適配器,如果外部客戶時(shí)MQ消息,那么適配器就是MQConsumer監(jiān)聽器,如果外部客戶時(shí)數(shù)據(jù)庫(kù),那么適配器可能就是Mybatis的Mapper。

隨著架構(gòu)的演化,后來(lái)又提出了洋蔥架構(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模式被稱為無(wú)副作用模式,熟悉函數(shù)時(shí)編程的朋友都知道,嚴(yán)格的函數(shù)就是一個(gè)無(wú)副作用的函數(shù),對(duì)于一個(gè)給定的輸入,總是返回固定的結(jié)果,通常查詢功能就是一個(gè)函數(shù),命令功能就不是一個(gè)函數(shù),它通常會(huì)執(zhí)行某些修改。

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

image

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

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

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

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

image

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

當(dāng)一個(gè)命令Command請(qǐng)求過(guò)來(lái)時(shí),會(huì)通過(guò)應(yīng)用層的CommandService去協(xié)調(diào)領(lǐng)域?qū)庸ぷ?,而一個(gè)查詢Query請(qǐng)求過(guò)來(lái)時(shí),則直接通過(guò)基礎(chǔ)實(shí)施的實(shí)現(xiàn)與數(shù)據(jù)庫(kù)或者外部服務(wù)交互。再次強(qiáng)調(diào),我們所有的抽象都定義在圓圈內(nèi)部,實(shí)現(xiàn)都在基礎(chǔ)設(shè)施

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

DDD、CQRS代碼落地

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

image

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

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

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

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àn)邏輯應(yīng)該放到哪一層的時(shí)候確定這一層代碼可以有請(qǐng)求參數(shù)的基本校驗(yàn),但是 應(yīng)用服務(wù)的校驗(yàn)邏輯是必須存在的,校驗(yàn)和應(yīng)用服務(wù)的耦合是緊密的。

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

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

  1. 校驗(yàn)
  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);
    }

}

我們?cè)倏纯磻?yīng)用服務(wù)的代碼發(fā)現(xiàn),發(fā)布領(lǐng)域事件的動(dòng)作放在了應(yīng)用層沒有放在領(lǐng)域?qū)?,而領(lǐng)域事件的定義是在領(lǐng)域?qū)?緊接著會(huì)提到),這是為什么呢?如果 不考慮持久化,發(fā)布領(lǐng)域事件的確應(yīng)該在領(lǐng)域模型中,但是在代碼落地時(shí),考慮到持久化完成后才代表有了真實(shí)的事件,所以我們將觸發(fā)事件的代碼放到了資源庫(kù)后面

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

我們采用了aggregate而不是model,是為了將聚合根的概念顯現(xiàn)出來(lái),每個(gè)聚合根單獨(dú)成一個(gè)子包,在單個(gè)聚合根中包含所需要的 值對(duì)象、領(lǐng)域事件的定義、資源庫(kù)的抽象接口等,這里解釋下為什么這些對(duì)象會(huì)在領(lǐng)域模型中,因?yàn)樗鼈兏荏w現(xiàn)這個(gè)領(lǐng)域模型,而且資源庫(kù)的抽象和聚合根有著對(duì)應(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;
    }

}

特別提醒的是,聚合根對(duì)象的創(chuàng)建不應(yīng)該被Spring容器管理,也不應(yīng)該被注入其它對(duì)象。我們注意到聚合根對(duì)象可以通過(guò)靜態(tài)工廠方法Factory Method來(lái)創(chuàng)建,下文還會(huì)介紹如何落地資源庫(kù)Repository進(jìn)行聚合根的創(chuàng)建。

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

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

  1. 不是屬于單個(gè)聚合根的業(yè)務(wù)或者需要多個(gè)聚合根配合的業(yè)務(wù),放在領(lǐng)域服務(wù)中,注意是業(yè)務(wù),如果沒有業(yè)務(wù),協(xié)調(diào)工作應(yīng)該放到應(yīng)用服務(wù)中
  2. 靜態(tài)方法放在領(lǐng)域服務(wù)中
  3. 需要通過(guò)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è)施可以對(duì)抽象的接口進(jìn)行實(shí)現(xiàn),上文中說(shuō)到資源庫(kù)Repository的接口定義在領(lǐng)域?qū)?,那么在基礎(chǔ)設(shè)施中就可以具體實(shí)現(xiàn)這個(gè)接口。

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);
        }
    }

}

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

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

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

application應(yīng)用服務(wù)包含了commond和query兩個(gè)子包,其實(shí)query可以提取出去和application平級(jí),但是這兩種做法沒有對(duì)錯(cuò),只是選擇問(wèn)題。

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

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

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

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

我們來(lái)看看一個(gè)查詢服務(wù)的實(shí)現(xiàn),其中CargoDTOAssembler是一個(gè)組裝器:

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);
    }
}

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

落地MQ、Event、Cache等

毫無(wú)疑問(wèn),MQ、Event、Cache的實(shí)現(xiàn)都應(yīng)該在基礎(chǔ)設(shè)施層,它們接口的定義放在哪里呢?一個(gè)方案是哪一層使用了抽象就在那一層定義接口,另一個(gè)方案是放到一個(gè)共有的抽象包下,基礎(chǔ)設(shè)施和對(duì)應(yīng)層依賴這個(gè)共有的抽象包。

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

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

package com.deepoove.cargo.shared;

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

然后在基礎(chǔ)實(shí)施中實(shí)現(xiàn),具體實(shí)現(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è)施層。

我們來(lái)看看具體監(jiān)聽器的實(shí)現(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和防腐層

前面提到過(guò),當(dāng)我們暴露一個(gè)RPC服務(wù)時(shí)和web層是平等對(duì)待的,比如暴露一個(gè)dubbo協(xié)議的服務(wù)就和暴露一個(gè)http的服務(wù)是平等的。這一小節(jié)我們將來(lái)探討如何與第三方系統(tǒng)的RPC服務(wù)進(jìn)行交互。

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

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

image

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

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

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

接下來(lái)我們來(lái)看看適配器的基本實(shí)現(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貨物實(shí)例和源碼

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

image

參考資料

在整個(gè)落地過(guò)程中,每次閱讀《領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)》和《實(shí)現(xiàn)領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)》這兩本書都會(huì)給我?guī)?lái)新的想法,值得推薦。
The Clean Architecture
DDD, Hexagonal, Onion, Clean, CQRS
dddsample-core

總結(jié)

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容