
前言
這篇文章假設(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ì):

其實(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)該依賴于抽象。

從圖中可以看到,基礎(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ì)。

如上圖,在這種架構(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。

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。

這張圖讀寫只是邏輯分離,物理層面還是使用了一個(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),如下圖所示:

架構(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ì)。

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)的,典型的處理流程是:
- 校驗(yàn)
- 協(xié)調(diào)領(lǐng)域模型或者領(lǐng)域服務(wù)
- 持久化
- 發(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)考慮:
- 不是屬于單個(gè)聚合根的業(yè)務(wù)或者需要多個(gè)聚合根配合的業(yè)務(wù),放在領(lǐng)域服務(wù)中,注意是業(yè)務(wù),如果沒有業(yè)務(wù),協(xié)調(diào)工作應(yīng)該放到應(yīng)用服務(wù)中
- 靜態(tài)方法放在領(lǐng)域服務(wù)中
- 需要通過(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è)辦法:
- 查詢服務(wù)中定義一套一摸一樣的DO,然后基礎(chǔ)設(shè)施做轉(zhuǎn)換,缺點(diǎn)是有點(diǎn)復(fù)雜,冗余了DO,優(yōu)點(diǎn)是完美符合DIP原則:抽象在查詢服務(wù)中,實(shí)現(xiàn)在基礎(chǔ)設(shè)施。
- 將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ì)》一書)所示:

圖中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

參考資料
在整個(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)的話題。