前言
這篇文章假設(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è)計:
其實這種分層架構(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)該依賴于抽象。
從圖中可以看到譬胎,基礎(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è)計。
如上圖包晰,在這種架構(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柏肪。
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算灸。
這張圖讀寫只是邏輯分離扼劈,物理層面還是使用了一個數(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)疑俭,如下圖所示:
架構(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è)計侵歇。
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)的娃善,典型的處理流程是:
- 校驗
- 協(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);
}
}
我們再看看應(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ù)中耻讽,可以從以下幾點考慮:
- 不是屬于單個聚合根的業(yè)務(wù)或者需要多個聚合根配合的業(yè)務(wù)察纯,放在領(lǐng)域服務(wù)中,注意是業(yè)務(wù)针肥,如果沒有業(yè)務(wù)饼记,協(xié)調(diào)工作應(yīng)該放到應(yīng)用服務(wù)中
- 靜態(tài)方法放在領(lǐng)域服務(wù)中
- 需要通過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ù)怎么可以訪問到這些對象呢胖烛?我們有兩個辦法:
- 查詢服務(wù)中定義一套一摸一樣的DO眼姐,然后基礎(chǔ)設(shè)施做轉(zhuǎn)換诅迷,缺點是有點復(fù)雜,冗余了DO妥凳,優(yōu)點是完美符合DIP原則:抽象在查詢服務(wù)中竟贯,實現(xiàn)在基礎(chǔ)設(shè)施答捕。
- 將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è)計》一書)所示:
圖中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
參考資料
在整個落地過程中来候,每次閱讀《領(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)的話題园欣。