來自亞馬遜的高級(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)庫(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ū)ο蠹采耄琄inesis 里定義了流(stream)和分片(shard)。先了解一個(gè)服務(wù)的領(lǐng)域?qū)ο笮窗耄俨榭?API 參考岸蜗,然后瀏覽服務(wù)的 API 清單。你會(huì)發(fā)現(xiàn)叠蝇,基于這些領(lǐng)域?qū)ο髽?gòu)建的 API 在理解和使用上都更加直觀璃岳。