為什么說要用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)庫(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 在理解和使用上都更加直觀璃岳。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市悔捶,隨后出現(xiàn)的幾起案子铃慷,更是在濱河造成了極大的恐慌,老刑警劉巖蜕该,帶你破解...
    沈念sama閱讀 217,509評(píng)論 6 504
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件犁柜,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡堂淡,警方通過查閱死者的電腦和手機(jī)馋缅,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,806評(píng)論 3 394
  • 文/潘曉璐 我一進(jìn)店門坛怪,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人股囊,你說我怎么就攤上這事「模” “怎么了稚疹?”我有些...
    開封第一講書人閱讀 163,875評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)祭务。 經(jīng)常有香客問我内狗,道長(zhǎng),這世上最難降的妖魔是什么义锥? 我笑而不...
    開封第一講書人閱讀 58,441評(píng)論 1 293
  • 正文 為了忘掉前任柳沙,我火速辦了婚禮,結(jié)果婚禮上拌倍,老公的妹妹穿的比我還像新娘赂鲤。我一直安慰自己,他們只是感情好柱恤,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,488評(píng)論 6 392
  • 文/花漫 我一把揭開白布数初。 她就那樣靜靜地躺著,像睡著了一般梗顺。 火紅的嫁衣襯著肌膚如雪泡孩。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,365評(píng)論 1 302
  • 那天寺谤,我揣著相機(jī)與錄音仑鸥,去河邊找鬼。 笑死变屁,一個(gè)胖子當(dāng)著我的面吹牛眼俊,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播敞贡,決...
    沈念sama閱讀 40,190評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼泵琳,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了誊役?” 一聲冷哼從身側(cè)響起获列,我...
    開封第一講書人閱讀 39,062評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎蛔垢,沒想到半個(gè)月后击孩,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,500評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡鹏漆,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,706評(píng)論 3 335
  • 正文 我和宋清朗相戀三年巩梢,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了创泄。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,834評(píng)論 1 347
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡括蝠,死狀恐怖鞠抑,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情忌警,我是刑警寧澤搁拙,帶...
    沈念sama閱讀 35,559評(píng)論 5 345
  • 正文 年R本政府宣布,位于F島的核電站法绵,受9級(jí)特大地震影響箕速,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜朋譬,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,167評(píng)論 3 328
  • 文/蒙蒙 一盐茎、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧徙赢,春花似錦字柠、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,779評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至阴汇,卻和暖如春数冬,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背搀庶。 一陣腳步聲響...
    開封第一講書人閱讀 32,912評(píng)論 1 269
  • 我被黑心中介騙來泰國(guó)打工拐纱, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人哥倔。 一個(gè)月前我還...
    沈念sama閱讀 47,958評(píng)論 2 370
  • 正文 我出身青樓秸架,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親咆蒿。 傳聞我的和親對(duì)象是個(gè)殘疾皇子东抹,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,779評(píng)論 2 354