架構(gòu)師必備 - DDD之落地實(shí)踐

哈嘍上煤,大家好休玩,我是啾啾。

今天帶大家認(rèn)識(shí)下DDD劫狠,一個(gè)聽起來很垃圾卻真的很牛X的設(shè)計(jì)思想拴疤,架構(gòu)師必備!

前言

在日常工作中独泞,接手或維護(hù)的工程呐矾,大多數(shù)使用的是三層架構(gòu),即controller懦砂、service蜒犯、dao三層,在使用的過程中荞膘,會(huì)遇到很多問題:

  • 面向數(shù)據(jù)建模罚随,面向過程編程,沒有真正“面向?qū)ο蟆?/li>
  • 只注重結(jié)果羽资,不注重過程淘菩,service層動(dòng)輒數(shù)百上千行,充斥著過程代碼屠升、膠水代碼潮改,要么臃腫、要么流水賬腹暖、要不重復(fù)汇在、要么邏輯分散,后期極難維護(hù)
  • 代碼耦合嚴(yán)重脏答,層與層之間互相調(diào)用糕殉、逆向調(diào)用亩鬼,牽一發(fā)而動(dòng)全身
  • 代碼無法體現(xiàn)業(yè)務(wù),在大家都不愛寫注釋的情況下阿蝶,隨著時(shí)間的推移辛孵,代碼業(yè)務(wù)邏輯將無人理解,不敢改也改不動(dòng)赡磅。

那么有沒有一個(gè)好的解決方案呢?今天要講的DDD就是一個(gè)不錯(cuò)的選擇宝与。

DDD

DDD焚廊,即領(lǐng)域驅(qū)動(dòng)設(shè)計(jì),完美的解決了以上問題:

  • 面向領(lǐng)域建模习劫,面向?qū)ο缶幊膛匚粒a直接映射現(xiàn)實(shí)世界概念,貼近業(yè)務(wù)诽里,離客戶更近
  • 領(lǐng)域邏輯高內(nèi)聚袒餐,符合Java開發(fā)原則
  • 技術(shù)細(xì)節(jié)變更如數(shù)據(jù)庫、緩存谤狡、定時(shí)器等的變更對(duì)業(yè)務(wù)邏輯影響比較小灸眼,非常適合插件式架構(gòu)
  • 代碼可讀性、可維護(hù)性更強(qiáng)墓懂,對(duì)后續(xù)擴(kuò)展焰宣、移植等支持更好,分層更加科學(xué)

DDD的概念捕仔,在網(wǎng)上很容易找到匕积,這里就不贅述了。

然而網(wǎng)上DDD的文章雖然很多榜跌,但大多數(shù)是理論知識(shí)闪唆,介紹的無非就是一些名詞:戰(zhàn)略設(shè)計(jì)、戰(zhàn)術(shù)設(shè)計(jì)钓葫、核心域悄蕾、支撐域、值對(duì)象瓤逼、實(shí)體笼吟、聚合... 對(duì)我們實(shí)際落地卻沒有太多的幫助,下面介紹下我在SpringBoot中應(yīng)用DDD的落地方案霸旗。

落地方案

1贷帮、代碼分層

image.png

代碼分層

  • 用戶接口層:圖中的api包(即controller層,我嫌controller后綴太長...)
  • 應(yīng)用層:這里使用了命令模式诱告,并且讀寫分離成了兩個(gè)包(command撵枢、query),如果不使用命令模式可以合并成一個(gè)service包
  • 領(lǐng)域?qū)?/strong>:domain包,使用JPA(對(duì)DDD有良好的支持)
  • 基礎(chǔ)設(shè)施層:infra包锄禽,其他所有的公用組件都放在這里潜必,如果使用DIP依賴倒置,那么實(shí)現(xiàn)類也放在這里沃但。
  • model模型:model包磁滚,用于存放不同層間傳遞的對(duì)象,這些對(duì)象我試過放到好些地方宵晚,最后發(fā)現(xiàn)還是提出來統(tǒng)一放在一個(gè)包下比較好(便于服務(wù)間調(diào)用時(shí)共用對(duì)象)

2垂攘、層級(jí)關(guān)系及模型傳遞

image.png

分層及調(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è)自定義注解后,對(duì)返回結(jié)果統(tǒng)一封裝

@Pagination加上這個(gè)自定義注解后逸贾,會(huì)自動(dòng)將分頁參數(shù)存入線程變量陨仅,后面查詢時(shí)也會(huì)自動(dòng)獲取分頁參數(shù),返回結(jié)果統(tǒng)一封裝時(shí)也會(huì)加上分頁信息

Qo是查詢參數(shù)對(duì)象铝侵,Dto是增刪改等命令參數(shù)對(duì)象灼伤,返回對(duì)象為Vo,這里要注意哟沫,Entity絕對(duì)不能暴露到這一層饺蔑,需要轉(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);
        // 對(duì)象模型轉(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ù)和充血對(duì)象方法

命令模式穴墅,實(shí)現(xiàn)自定義Command接口,泛型為返回值

通過屬性和構(gòu)造方法(使用lombok注解)接收參數(shù)

一個(gè)命令里只有一個(gè)execute方法温自,缺點(diǎn)是會(huì)產(chǎn)生大量的命令類玄货,一個(gè)類相當(dāng)于之前service類中的一個(gè)方法,但是這樣符合了單一職責(zé)原則

通過executor.getRecerver方法獲取到領(lǐng)域服務(wù)(manager)

DTO絕對(duì)不下探到領(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ū)?/p>

由于JPA對(duì)于復(fù)雜查詢不太好用复斥,這里強(qiáng)烈推薦使用QueryDSL(以后再單獨(dú)細(xì)講),圖中是一個(gè)簡單的使用例子

  • domain包

類較多械媒,代碼部分不一一羅列:

由Entity類自動(dòng)生成數(shù)據(jù)庫表目锭,僅維護(hù)Entity類(屏蔽數(shù)據(jù)庫)

設(shè)計(jì)Entity時(shí)根據(jù)實(shí)際業(yè)務(wù)靈活使用@OneToMany、@OneToOne等注解(聚合根的概念)

聚合根不要太大纷捞,80%的情況一個(gè)聚合根中只包含一個(gè)實(shí)體(不要過度設(shè)計(jì)成大聚合根)

不要使用貧血模型侣集,而是要面向?qū)ο螅瑢儆趯?duì)象的方法要放到對(duì)象中兰绣,但是對(duì)象中不建議引入倉庫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對(duì)業(yè)務(wù)理解更加透徹踪央,寫的代碼可以更好的傳達(dá)客戶的業(yè)務(wù)訴求
  • 能夠盡情地編寫低耦合的臀玄、符合單一職責(zé)、開閉等原則畅蹂、封裝健无、繼承、多態(tài)的代碼液斜,是很身心愉悅的
  • 前期相比傳統(tǒng)架構(gòu)代碼量更多累贤,開發(fā)人員前期投入更多:
  • 領(lǐng)域的合理劃分、實(shí)體的合理設(shè)計(jì)
  • 大量的DTO少漆、VO等數(shù)據(jù)對(duì)象
  • 大量的數(shù)據(jù)對(duì)象轉(zhuǎn)換方法
  • 大量的命令類
  • ...

但是臼膏,除非是特別簡單的功能,對(duì)于一個(gè)中等復(fù)雜的系統(tǒng)示损,這些前期的付出還是值得的渗磅,一張圖說明:

小結(jié)

以上簡單介紹了下我對(duì)DDD的理解和實(shí)踐,并通過實(shí)際的代碼展現(xiàn)了如何在SpringBoot中應(yīng)用DDD检访,希望能為大家提供一個(gè)思路

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末始鱼,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子脆贵,更是在濱河造成了極大的恐慌医清,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,941評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件丹禀,死亡現(xiàn)場(chǎng)離奇詭異状勤,居然都是意外死亡鞋怀,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,397評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門持搜,熙熙樓的掌柜王于貴愁眉苦臉地迎上來密似,“玉大人,你說我怎么就攤上這事葫盼〔须纾” “怎么了?”我有些...
    開封第一講書人閱讀 165,345評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵贫导,是天一觀的道長抛猫。 經(jīng)常有香客問我,道長孩灯,這世上最難降的妖魔是什么闺金? 我笑而不...
    開封第一講書人閱讀 58,851評(píng)論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮峰档,結(jié)果婚禮上败匹,老公的妹妹穿的比我還像新娘。我一直安慰自己讥巡,他們只是感情好掀亩,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,868評(píng)論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著欢顷,像睡著了一般槽棍。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上抬驴,一...
    開封第一講書人閱讀 51,688評(píng)論 1 305
  • 那天炼七,我揣著相機(jī)與錄音,去河邊找鬼布持。 笑死特石,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的鳖链。 我是一名探鬼主播姆蘸,決...
    沈念sama閱讀 40,414評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼芙委!你這毒婦竟也來了逞敷?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,319評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤灌侣,失蹤者是張志新(化名)和其女友劉穎推捐,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體侧啼,經(jīng)...
    沈念sama閱讀 45,775評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡牛柒,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,945評(píng)論 3 336
  • 正文 我和宋清朗相戀三年堪簿,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片皮壁。...
    茶點(diǎn)故事閱讀 40,096評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡椭更,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出蛾魄,到底是詐尸還是另有隱情虑瀑,我是刑警寧澤,帶...
    沈念sama閱讀 35,789評(píng)論 5 346
  • 正文 年R本政府宣布滴须,位于F島的核電站舌狗,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏扔水。R本人自食惡果不足惜痛侍,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,437評(píng)論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望魔市。 院中可真熱鬧恋日,春花似錦、人聲如沸嘹狞。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,993評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽磅网。三九已至,卻和暖如春筷屡,著一層夾襖步出監(jiān)牢的瞬間涧偷,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,107評(píng)論 1 271
  • 我被黑心中介騙來泰國打工毙死, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留燎潮,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,308評(píng)論 3 372
  • 正文 我出身青樓确封,卻偏偏與公主長得像,于是被迫代替她去往敵國和親爪喘。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,037評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容