領(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),它負責協(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放在如何表達領(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)該依賴于細節(jié),細節(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ū)恿?,下文會再闡述如何落地Repository。

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

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

image

如上圖,在這種架構(gòu)風(fēng)格中,外部客戶和內(nèi)部系統(tǒng)的交互都會通過端口和適配器完成轉(zhuǎn)換,這些外部客戶之間是平等的,比如用戶web界面和數(shù)據(jù)庫持久化,當你需要一個新的外部客戶時,只需要增加相應(yīng)的適配器,比如當我們增加外部一個RPC的服務(wù)時,只需要編寫對應(yī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)層知道外層的細節(jié),下圖是整潔架構(gòu)圖,詳細介紹這里就不作贅述,可以參考這篇文章:The Clean Architecture。

image

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

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

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

image

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

最終CQRS落地的方案我們選擇了簡單化處理,物理層面還是使用一個數(shù)據(jù)庫,查詢的時候部分數(shù)據(jù)直接從數(shù)據(jù)庫讀取,部分數(shù)據(jù)使用到了Elasticsearch,當數(shù)據(jù)庫發(fā)生更改時,會發(fā)送Event事件通知ES進行更新。當然我們還可以更加技術(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)部的抽象。

當一個命令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)的好處就在于,當增加新的用戶的界面時我們可以創(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ù)功能,此處不包含任何邏輯。我們認為應(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ū)?,而領(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,當持久化時就需要將聚合根 序列化 成數(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返回給用戶界面這個要看情況,個人認為當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)該充當著適配器的作用,所以它們的落地都應(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和防腐層

前面提到過,當我們暴露一個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ù)足夠細致的年代,我們可以做到一個微服務(wù)就代表著一個限界上下文。

當我們與第三方系統(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é)

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

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

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

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