在日常工作中,接手或維護(hù)的工程,大多數(shù)使用的是三層架構(gòu),即controller、service、dao三層,在使用的過程中,會遇到很多問題:
- 面向數(shù)據(jù)建模,面向過程編程,沒有真正“面向?qū)ο蟆?/li>
- 只注重結(jié)果,不注重過程,service層動輒數(shù)百上千行,充斥著過程代碼、膠水代碼,要么臃腫、要么流水賬、要不重復(fù)、要么邏輯分散,后期極難維護(hù)
- 代碼耦合嚴(yán)重,層與層之間互相調(diào)用、逆向調(diào)用,牽一發(fā)而動全身
- 代碼無法體現(xiàn)業(yè)務(wù),在大家都不愛寫注釋的情況下,隨著時(shí)間的推移,代碼業(yè)務(wù)邏輯將無人理解,不敢改也改不動。
那么有沒有一個(gè)好的解決方案呢?今天要講的DDD就是一個(gè)不錯(cuò)的選擇。
DDD
DDD,即 領(lǐng)域驅(qū)動設(shè)計(jì) ,完美的解決了以上問題:
- 面向領(lǐng)域建模,面向?qū)ο缶幊蹋a直接映射現(xiàn)實(shí)世界概念,貼近業(yè)務(wù),離客戶更近
- 領(lǐng)域邏輯高內(nèi)聚,符合Java開發(fā)原則
- 技術(shù)細(xì)節(jié)變更如數(shù)據(jù)庫、緩存、定時(shí)器等的變更對業(yè)務(wù)邏輯影響比較小,非常適合插件式架構(gòu)
- 代碼可讀性、可維護(hù)性更強(qiáng),對后續(xù)擴(kuò)展、移植等支持更好,分層更加科學(xué)
DDD的概念,在網(wǎng)上很容易找到,這里就不贅述了。
然而網(wǎng)上DDD的文章雖然很多,但大多數(shù)是理論知識,介紹的無非就是一些名詞:戰(zhàn)略設(shè)計(jì)、戰(zhàn)術(shù)設(shè)計(jì)、核心域、支撐域、值對象、實(shí)體、聚合... 對我們實(shí)際落地卻沒有太多的幫助,下面介紹下我在SpringBoot中應(yīng)用DDD的落地方案。
落地方案
1、代碼分層
代碼分層
- 用戶接口層 :圖中的api包(即controller層,我嫌controller后綴太長...)
- 應(yīng)用層 :這里使用了命令模式,并且讀寫分離成了兩個(gè)包(command、query),如果不使用命令模式可以合并成一個(gè)service包
- 領(lǐng)域?qū)?/strong> :domain包,使用JPA(對DDD有良好的支持)
- 基礎(chǔ)設(shè)施層 :infra包,其他所有的公用組件都放在這里,如果使用DIP依賴倒置,那么實(shí)現(xiàn)類也放在這里。
- model模型 :model包,用于存放不同層間傳遞的對象,這些對象我試過放到好些地方,最后發(fā)現(xiàn)還是提出來統(tǒng)一放在一個(gè)包下比較好(便于服務(wù)間調(diào)用時(shí)共用對象)
2、層級關(guān)系及模型傳遞
分層及調(diào)用關(guān)系
3、分層詳細(xì)說明
api包(controller)
@Tag(name = "用戶", description = "用戶")
@RestController
@RequestMapping(value = "/api/sys-user")
public class SysUserApi extends BaseApi {
@ApiResult
@Operation(summary = "根據(jù)ID查詢用戶")
@GetMapping("/{id}")
public SysUserVo get(@PathVariable Long id) {
return queryExecutor.execute(new SysUserByIdQry(id));
}
@Pagination(total = true)
@ApiResult
@Operation(summary = "分頁查詢用戶")
@GetMapping
public List< SysUserVo > getList(SysUserQo sysUserQo) {
return queryExecutor.execute(new SysUserListQry(sysUserQo));
}
@ApiResult
@Operation(summary = "新增用戶")
@PostMapping
public void save(@Valid @RequestBody SysUserDto sysUserDto) {
commandExecutor.execute(new SysUserCommonCmd(sysUserDto));
}
}
- 在BaseApi中封裝了兩個(gè)命令執(zhí)行類queryExecutor和commandExecutor,調(diào)用應(yīng)用層時(shí)執(zhí)行不同的命令即可,無需@Autowired引入不同的服務(wù)
- @ApiResult加上這個(gè)自定義注解后,對返回結(jié)果統(tǒng)一封裝
- @Pagination加上這個(gè)自定義注解后,會自動將分頁參數(shù)存入線程變量,后面查詢時(shí)也會自動獲取分頁參數(shù),返回結(jié)果統(tǒng)一封裝時(shí)也會加上分頁信息
- Qo是查詢參數(shù)對象,Dto是增刪改等命令參數(shù)對象,返回對象為Vo,這里要注意,Entity絕對不能暴露到這一層,需要轉(zhuǎn)換為Vo再返回
- 在這一層中,每個(gè)方法幾乎就是一行執(zhí)行命令的語句,一般情況不進(jìn)行業(yè)務(wù)邏輯(當(dāng)然也有特殊情況咯)
command包
@AllArgsConstructor
public class SysDeptAddCmd implements Command Void > {
private SysDeptDto sysDeptDto;
@Override
public Void execute(Executor executor) {
// 獲取命令的接收者:領(lǐng)域服務(wù)
SysDeptManager receiver = executor.getReceiver(SysDeptManager.class);
// 對象模型轉(zhuǎn)換,由DTO轉(zhuǎn)為Entity,使用了MapStruct
SysDept sysDept = SysDeptMapper.INSTANCE.toSysDept(sysDeptDto);
// 使用JPA保存
receiver.save(sysDept);
return null;
}
}
- 增刪改命令,很薄的一層,作為一項(xiàng)工作的組織者,幾乎沒有業(yè)務(wù)邏輯,調(diào)用領(lǐng)域服務(wù)和充血對象方法
- 命令模式,實(shí)現(xiàn)自定義Command接口,泛型為返回值
- 通過屬性和構(gòu)造方法(使用lombok注解)接收參數(shù)
- 一個(gè)命令里只有一個(gè)execute方法,缺點(diǎn)是會產(chǎn)生大量的命令類,一個(gè)類相當(dāng)于之前service類中的一個(gè)方法,但是這樣符合了單一職責(zé)原則
- 通過executor.getRecerver方法獲取到領(lǐng)域服務(wù)(manager)
- DTO絕對不下探到領(lǐng)域?qū)又校枰扔蒁TO轉(zhuǎn)換為Entity(轉(zhuǎn)換方法這里使用的MapStruct,以后再單獨(dú)細(xì)講)
query包
@AllArgsConstructor
public class SysDeptByIdQry extends CommonQry< SysDeptVo > {
private Long id;
@Override
public SysDeptVo execute(Executor executor) {
if (id == null) {
throw new BusinessException("部門ID不能為空");
}
QSysDept sysDept = QSysDept.sysDept;
return queryFactory.select(this.fields())
.from(sysDept)
.where(sysDept.deleted.eq(false), sysDept.id.eq(id))
.fetchOne();
}
/**
* 部門VO映射
*
* @return QBean< SysDeptVo >
*/
public static QBean< SysDeptVo > fields() {
QSysDept sysDept = QSysDept.sysDept;
return Projections.fields(
SysDeptVo.class,
sysDept.deptName,
sysDept.orderNum,
sysDept.id
);
}
}
- 命令模式,繼承自定義CommonQry基類(此類也實(shí)現(xiàn)了自定義的Command接口,其中引用了QueryDSL的queryFactory類,且封裝了分頁方法),泛型為返回值
- query包中的查詢命令與command包中的命令大體相同,唯一區(qū)別是query命令理論上直接查詢數(shù)據(jù)庫,不調(diào)用領(lǐng)域?qū)?/li>
- 由于JPA對于復(fù)雜查詢不太好用,這里強(qiáng)烈推薦使用QueryDSL(以后再單獨(dú)細(xì)講),圖中是一個(gè)簡單的使用例子
domain包
類較多,代碼部分不一一羅列:
- 由Entity類自動生成數(shù)據(jù)庫表,僅維護(hù)Entity類(屏蔽數(shù)據(jù)庫)
- 設(shè)計(jì)Entity時(shí)根據(jù)實(shí)際業(yè)務(wù)靈活使用@OneToMany、@OneToOne等注解(聚合根的概念)
- 聚合根不要太大,80%的情況一個(gè)聚合根中只包含一個(gè)實(shí)體(不要過度設(shè)計(jì)成大聚合根)
- 不要使用貧血模型,而是要面向?qū)ο螅瑢儆趯ο蟮姆椒ㄒ诺綄ο笾校菍ο笾胁唤ㄗh引入倉庫repository類,需要操作數(shù)據(jù)庫的方法寫在領(lǐng)域服務(wù)manager里
- 業(yè)務(wù)邏輯盡量寫在領(lǐng)域服務(wù)(manager)中,不斷提取、抽象不同的方法供應(yīng)用層調(diào)用
- 適當(dāng)?shù)氖褂妙I(lǐng)域事件,JPA可以在Entity中使用@DomainEvents注解來發(fā)送領(lǐng)域事件
心得
- 通過DDD對業(yè)務(wù)理解更加透徹,寫的代碼可以更好的傳達(dá)客戶的業(yè)務(wù)訴求
- 能夠盡情的編寫低耦合的、符合單一職責(zé)、開閉等原則、封裝、繼承、多態(tài)的代碼,是很身心愉悅的
- 前期相比傳統(tǒng)架構(gòu)代碼量更多,開發(fā)人員前期投入更多:
- 領(lǐng)域的合理劃分、實(shí)體的合理設(shè)計(jì)
- 大量的DTO、VO等數(shù)據(jù)對象
- 大量的數(shù)據(jù)對象轉(zhuǎn)換方法
- 大量的命令類
- ...
- 但是,除非是特別簡單的功能,對于一個(gè)中等復(fù)雜的系統(tǒng),這些前期的付出還是值得的,一張圖說明:
小結(jié)
以上簡單介紹了下我對DDD的理解和實(shí)踐,并通過實(shí)際的代碼展現(xiàn)了如何在SpringBoot中應(yīng)用DDD,希望能為大家提供一個(gè)思路。
-
編程
+關(guān)注
關(guān)注
88文章
3621瀏覽量
93785 -
代碼
+關(guān)注
關(guān)注
30文章
4793瀏覽量
68702 -
ddd
+關(guān)注
關(guān)注
0文章
23瀏覽量
2933 -
SpringBoot
+關(guān)注
關(guān)注
0文章
173瀏覽量
183
發(fā)布評論請先 登錄
相關(guān)推薦
評論