為什么說要用DDD替代CRUD來設(shè)計(jì)API

來自亞馬遜的高級(jí)工程師 James Hood 以簡(jiǎn)單明了的例子說明了為什么要用 DDD 替代 CRUD 來設(shè)計(jì) REST API。他提到“DDD 與 REST API 近乎天然地合拍,因?yàn)?REST 的資源可以很好地與 DDD 的實(shí)體映射起來”。

REST 以資源為中心,這些資源以 URI 的形式呈現(xiàn)。在調(diào)用 HTTP 時(shí),通過指定一個(gè) HTTP 動(dòng)詞和一個(gè)資源 URI 對(duì)某個(gè)特定的資源進(jìn)行操作。大部分 REST 框架都提供了生成器,你只要指定一個(gè)資源的名字,框架就會(huì)為你生成腳手架(scaffold)。

不過,這些生成器默認(rèn)使用的是 CRUD 模型(Create、Read、Update、Delete),它們把資源看成是一系列屬性的集合,使用 JSON 或與特定語言相關(guān)的數(shù)據(jù)對(duì)象來表示資源,并生成用于對(duì)資源進(jìn)行創(chuàng)建、讀取、更新和刪除操作的方法。

雖然這給開發(fā)者帶來了便利,但我覺得這樣是有問題的。我不喜歡 CRUD 這樣的說法,尤其不喜歡當(dāng)中的 U。

問題:CRUD 中的 U

一般的更新操作允許客戶端更新資源的任何一個(gè)字段,并使用新版本覆蓋已有的版本。但如果你允許客戶端這么做,那么你的服務(wù) API 就失去了應(yīng)有的價(jià)值。

服務(wù)層的一個(gè)關(guān)鍵價(jià)值在于為底層的數(shù)據(jù)增加業(yè)務(wù)約束,因此,資源最終都需要帶上業(yè)務(wù)約束。

那么,難道我們就不能給更新操作增加業(yè)務(wù)約束嗎?讓我們以最簡(jiǎn)單的銀行賬戶為例。首先,不能讓客戶通過調(diào)用 API 來隨意更新他們的賬戶余額。另外,賬戶或許需要最小余額的限制。

你在更新操作里做了一些檢查,賬戶余額的變動(dòng)必須發(fā)生在一個(gè)指定的范圍內(nèi)。那么這樣問題就解決了嗎?當(dāng)然沒有。任何一次余額的調(diào)整都需要與某種事務(wù)相對(duì)應(yīng),不是嗎?是存入、取出,還是轉(zhuǎn)賬?如果客戶要更改賬戶該怎么辦?這樣做是被允許的嗎?這樣做會(huì)不會(huì)破壞與其他數(shù)據(jù)之間的關(guān)系?

不難看出,你的更新操作很快會(huì)讓這一切變得像意大利面條一樣混亂不堪。我曾經(jīng)看著一些團(tuán)隊(duì)走上了這條不歸路,他們?cè)噲D從更新的字段里去推測(cè)客戶的意圖,結(jié)果代碼變得像團(tuán)亂麻。

解決方法:DDD

那么該如何解決這個(gè)問題,有其他更好的方案嗎?我個(gè)人更喜歡基于領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)(DDD)來設(shè)計(jì) API。DDD 的基本思想是說,軟件的建模應(yīng)該發(fā)生在真實(shí)世界的問題得到解決之后。

DDD 使用實(shí)體(Entity)和聚合(Aggregate)來描述業(yè)務(wù)對(duì)象,還定義了服務(wù)(Service)、值對(duì)象(Value Object)和倉(cāng)庫(kù)(Repository)等術(shù)語,用以解決業(yè)務(wù)領(lǐng)域或 DDD 邊界上下文問題。

DDD 不一定非要與 REST 綁定在一起,不過我發(fā)現(xiàn)?DDD 與 REST API 近乎天然地合拍,因?yàn)?REST 的資源可以很好地與 DDD 的實(shí)體映射起來。

那么這意味著什么呢?這意味著,你的 API 應(yīng)該要以?領(lǐng)域?qū)ο?/b>?以及這些對(duì)象所提供的?業(yè)務(wù)操作為中心。業(yè)務(wù)操作是對(duì)常規(guī)更新操作最好的替代品。我們繼續(xù)以之前的銀行賬戶為例。

對(duì)于銀行的 API 來說,賬戶就是一個(gè)領(lǐng)域?qū)ο螅―DD 里的實(shí)體)。這次我們不再使用 CRUD 來為賬戶建模,而是為賬戶定義一組業(yè)務(wù)操作。以下是一系列寫入操作:

開戶(Open)——新開一個(gè)賬戶。

銷戶(Close)——注銷一個(gè)已有的賬戶。

取出(Debit)——從賬戶里扣掉一些錢。

存入(Credit)——往賬戶里存入一些錢。

這些操作都帶有一定的?業(yè)務(wù)約束。例如,往一個(gè)已經(jīng)注銷的賬戶里存錢是不被允許的,而在取錢的時(shí)候要強(qiáng)制檢查最小余額。至于讀取操作,我們可以為客戶提供一些有用的查詢:

加載——通過賬戶 ID 加載相應(yīng)的賬戶信息。

交易歷史——列出賬戶的交易歷史。

客戶的賬戶列表——列出指定客戶的所有賬戶。

在定義好業(yè)務(wù)操作之后,就可以將它們與 REST API 映射起來:

POST /account ——新開一個(gè)賬戶。

PUT /account//close ——注銷一個(gè)已有的賬戶。

PUT /account//debit ——從賬戶里扣掉一些錢。

PUT /account//credit ——往賬戶里存入一些錢。

GET /account/——通過賬戶 ID 加載相應(yīng)的賬戶信息。

GET /account//transactions ——列出賬戶的交易歷史。

GET /accounts/query/customerId/——列出指定客戶的所有賬戶。

這些看起來與一般的 CRUD API 非常不一樣,關(guān)鍵在于這些操作具有良好的定義。不管對(duì)于?服務(wù)提供方?還是?客戶端?來說,這樣的體驗(yàn)都更好。

服務(wù)提供方不再需要根據(jù)更新字段來推測(cè)業(yè)務(wù)操作的意圖,業(yè)務(wù)操作清晰明了,這樣的代碼更簡(jiǎn)單,也更容易維護(hù)。

而對(duì)于客戶端來說,它們能執(zhí)行或不能執(zhí)行哪些操作也是一目了然的。如果 API 具有良好的文檔化,比如使用了 Swagger,那么就可以很清楚地了解到 API 都具有哪些約束。

定義這樣的 API 需要做一些前期思考,這不同于使用簡(jiǎn)單的 CRUD 生成器。如果你打算將 API 暴露成公共端點(diǎn),就需要在很長(zhǎng)的一段時(shí)間內(nèi)為 API 提供支持,最好還是把它看成是一個(gè)永久性的事項(xiàng)。

我總是建議人們?cè)谇捌诙嗷ㄒ稽c(diǎn)時(shí)間,因?yàn)橛行〇|西到了后面就很難修改,而 API 就是一個(gè)很好的例子。

所以,在進(jìn)行 API(REST 或其他)設(shè)計(jì)時(shí),請(qǐng)停止使用 CRUD 模型。相反,可以通過 DDD 來定義 API,包括領(lǐng)域?qū)ο蠛退鼈兊臉I(yè)務(wù)操作。

如果你想看到更多關(guān)于領(lǐng)域?qū)ο蟮睦?,可以參?Amazon Web Services 的 API。在 AWS API 開發(fā)者指南里,每一個(gè)服務(wù)都有對(duì)應(yīng)的“關(guān)鍵概念”一節(jié),用以描述領(lǐng)域?qū)ο蟆?/p>

例如,S3 里定義了 Bucket、Object 和 Permission 等領(lǐng)域?qū)ο?,Kinesis 里定義了流(stream)和分片(shard)。先了解一個(gè)服務(wù)的領(lǐng)域?qū)ο?,再查?API 參考,然后瀏覽服務(wù)的 API 清單。你會(huì)發(fā)現(xiàn),基于這些領(lǐng)域?qū)ο髽?gòu)建的 API 在理解和使用上都更加直觀。

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

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