使用Domain-Driven創(chuàng)建Hypermedia API
在現(xiàn)實(shí)中我們會(huì)遇到各種各樣的復(fù)雜場景,"There is not a right way" 用來描述API的設(shè)計(jì)方法再合適不過了,沒有一種API設(shè)計(jì)方式可以應(yīng)對(duì)所有的場景。區(qū)別于"Consumer-Driven Contract",本文將描述另外一種設(shè)計(jì)API的方式:Domain-Driven API。這不是API設(shè)計(jì)的標(biāo)準(zhǔn)方法,但是他也許可以給你靈感,幫助你設(shè)計(jì)出更加具有表達(dá)力的API。
POST /api/customer
POST /api/customer/order
PUT /api/customer
POST /api/customer/notification
上圖是一個(gè)API文檔片段,他們通過HTTP動(dòng)作加上統(tǒng)一資源標(biāo)識(shí)符(URI)來描述自己的意圖,也許還需要一份不錯(cuò)的文檔來描述他的參數(shù),返回類型等,就能被客戶端調(diào)用和使用。市面上也有類似Swager這樣高效的產(chǎn)品,用起來也很方便。但是這樣的API或多或少有一些設(shè)計(jì)方面的小問題:
- 無法通過API描述上下文
縱然一個(gè)HTTP動(dòng)詞加上一個(gè)描述API資源的名詞基本可以能夠描述其意圖,但是在使用過程中一份API文檔似乎還是少不了。在過去的若干年里我去掉了給代碼寫注釋的壞毛病,因?yàn)槲艺J(rèn)識(shí)到良好的組織結(jié)構(gòu)和代碼是自描述的。然而當(dāng)我們?cè)O(shè)計(jì)API的時(shí)候,大家不約而同的接受了編寫文檔的事實(shí)。在"Consumer-Driven Contract"過程中還要編寫一份契約測試來驅(qū)動(dòng)服務(wù)端保證契約的一致性。有沒有可能讓API資源包含這一份契約,同時(shí)讓消費(fèi)者去遵守契約呢?
- API消費(fèi)端知道的太多
在上圖的API文檔片段中,你知道應(yīng)該在什么時(shí)候調(diào)用下面的API嗎?
POST /api/customer/notification
你可能不知道,也許是當(dāng)用戶下了訂單,也或者是用戶支付了訂單,這取決于需求。似乎看起來合情合理,但是這樣的場景預(yù)示著一部分領(lǐng)域邏輯有轉(zhuǎn)移到消費(fèi)端的嫌疑。打個(gè)比方,你去飯店吃飯,服務(wù)員拿過來了一個(gè)菜單,當(dāng)你點(diǎn)了一分湯的時(shí)候服務(wù)員告訴你這個(gè)菜單有自己的規(guī)則,只有你先點(diǎn)一份蛋炒飯,你才能夠點(diǎn)這份湯。這時(shí)候你只有一種選擇那就是記住這個(gè)規(guī)則,下次先點(diǎn)蛋炒飯。有沒有可能不要把這個(gè)規(guī)則強(qiáng)加在消費(fèi)端呢?
- 易碎的設(shè)計(jì)
API以提供URI的方式來提供服務(wù),而URI在本質(zhì)上就是一個(gè)字符串,作為一個(gè)強(qiáng)類型玩家,我不希望這樣的字符串分散在各個(gè)角落,試想我重命名了一個(gè)URI,我不得不搜索并修改所有曾經(jīng)使用過這個(gè)資源的代碼。
一、設(shè)計(jì)Domain
我們?cè)趯?shí)踐領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)時(shí)我們?cè)谧鍪裁矗空页鲱I(lǐng)域邊界、找到聚合根、劃分Domain、根據(jù)Domain的能力做出抽象并設(shè)計(jì)良好的模型。而Domain在提供業(yè)務(wù)需求的過程就是Domain模型狀態(tài)發(fā)生變化的過程。
同樣的道理,我們?cè)O(shè)計(jì)API是為了達(dá)到什么目的?我希望我的API不但能夠完成增刪改查,還能夠更具表達(dá)力。每一個(gè)API不是獨(dú)立存在的,他們是Domain模型在某一時(shí)刻狀態(tài)和能力的體現(xiàn),每一個(gè)API資源在告知消費(fèi)者目前Domain狀態(tài)的同時(shí),還可以告訴消費(fèi)者當(dāng)前Domain具備了什么樣的能力,消費(fèi)者接下來能夠做什么,也即消費(fèi)者能夠請(qǐng)求哪一個(gè)API資源。
這么說來API的設(shè)計(jì)實(shí)際上跟Domain能力的設(shè)計(jì)有千絲萬縷的關(guān)系,我決定用航空公司的賣票業(yè)務(wù)來舉例說明。
業(yè)務(wù)需求:
一個(gè)叫做RestAirline的航空公司提供在線機(jī)票出售業(yè)務(wù),用戶可以按照搜索條件搜索到所有可用的航班(trip)
當(dāng)乘客選中一條可用的航班(trip)就開始了整個(gè)預(yù)定(booking)流程
一旦乘客選擇了一條可用的航班就可以修改航班(change trip)和選擇座位(seat)
當(dāng)乘客選擇完座位還可以添加一些額外的服務(wù),如:接送機(jī)服務(wù)(transfer service)等, 最后完成支付(payment)
乘客在飛機(jī)起飛前還可以做在線登機(jī)手續(xù)(checkin)并打印登機(jī)牌(boardingpass),在Checkin的過程中還可以重新選擇座位
注意:括號(hào)中的英文術(shù)語可以理解為該公司的Domain language, 我們?cè)陬I(lǐng)域建模的時(shí)候也會(huì)使用相同的術(shù)語,從而減少跟領(lǐng)域?qū)<业臏贤ǔ杀尽?/p>
就上面的需求我們可以很容易的分析出若干個(gè)Domain: Booking, Payment, Trip Avalability
- 設(shè)計(jì)Booking Domain
我們以Booking Domain為例來描述設(shè)計(jì)過程,下面的交互圖清晰的描述出了Booking的能力
- 實(shí)現(xiàn)Booking Domain
實(shí)現(xiàn)過程也相當(dāng)?shù)闹苯樱绻麑⑾旅娴拇a閱讀出來,幾乎跟之前描述的業(yè)務(wù)需求是完全匹配的。Booking Domain的實(shí)現(xiàn)需要注意下面幾點(diǎn):
所有屬性都是private set,意味著Domain的內(nèi)部屬性是靠自己維護(hù)的;
AirportTransfer為`Maybe
該類的構(gòu)造函數(shù)被修飾為private,意味著Booking只能通過選擇可用的航班來創(chuàng)建,代碼的含義詮釋了業(yè)務(wù)需求
Checkin被設(shè)計(jì)為Sub-Domain,因?yàn)镃heckin的實(shí)現(xiàn)過程略微復(fù)雜,是否是一個(gè)Sub-Domain取決于設(shè)計(jì);
public?class?Booking { ????public?Guid?Id?{?get;?} ????public?IReadOnlyList
二、 設(shè)計(jì)具有Domain能力的API
根據(jù)上面設(shè)計(jì)好的Domain,我們可以輕松設(shè)計(jì)出第一個(gè)表達(dá)Domain能力的API: tripselection:
POST /api/booking/tripselection
實(shí)際上這一API的實(shí)現(xiàn)方式就是直接調(diào)用對(duì)應(yīng)的Domain能力:
var?booking?=?Booking.SelectTrip(trip,?passengers)
站在Domain的角度,這一能力創(chuàng)建了一個(gè)Booking,同時(shí)還將一個(gè)可用的航班(Trip)和乘客列表添加到了Booking中,
此時(shí)的Booking就擁有了一些初始狀態(tài),同時(shí)還具備了一定的能力:分配座位(seat)和修改航班(flight)。
站在API消費(fèi)者的角度,在消費(fèi)者消費(fèi)完畢tripselection這個(gè)API之后,除了能夠得到一些必要的返回值,還擁有了調(diào)用下面三個(gè)API的能力:
GET api/booking/{id}
PUT api/booking/{id}/seatassignment
PUT api/booking/{id}/changeflight
這三個(gè)API跟Domain在此時(shí)擁有的能力是一致的。**Hypermedia API的思想在于:API資源除了包含必要的返回值,還能告訴API消費(fèi)者下一步Domain擁有的能力和此時(shí)Domain的狀態(tài),也就是API消費(fèi)者接下來可以請(qǐng)求什么樣的API。
三、實(shí)現(xiàn)Hypermedia API
根據(jù)上面的分析,我們嘗試對(duì)tripselection API返回的資源進(jìn)行第一版建模,一個(gè)最初的版本如下:
public?class?TripSelectionResource { ????private?readonly?IUrlHelper?_urlHelper; ????public?TripSelectionResource(IUrlHelper?urlHelper) ????{ ????????_urlHelper?=?urlHelper; ????} ????public?Guid?BookingId?{?get;?set;?} ????public?string?BookingResource?=>?_urlHelper.Action("GetBooking",?"Booking"); ????public?string?FlightChange?=>?_urlHelper.Action("ChangeFlight",?"Booking"); ????public?string?SeatAssignment?=>?_urlHelper.Action("AssignSeat",?"Booking"); }
其中 BookingResource,F(xiàn)lightChange,SeatAssignment 分別為對(duì)應(yīng)的API URI地址,使用了ASP.NET Web API提供的 urlHelper.Action("ActionName","ControllerName") 方法來生成一個(gè)url。
理論上所有的API都能劃分為兩類,Command`和`Query(參考CQRS pattern),其中能夠改變Domain狀態(tài)的API都可以認(rèn)為是API消費(fèi)者發(fā)送了一個(gè)Command;另一類API則可以劃分到Query,無論API消費(fèi)者請(qǐng)求多少遍都不會(huì)改變Domain的狀態(tài),通常指Get請(qǐng)求。
針對(duì)TripSelectionResource包含的三個(gè)API,我們也可以將其劃分為兩類:
public?class?TripSelectionResource { ????private?readonly?IUrlHelper?_urlHelper; ????public?TripSelectionResource(IUrlHelper?urlHelper) ????{ ????????_urlHelper?=?urlHelper; ????} ????public?Guid?BookingId?{?get;?set;?} ????public?Link
一個(gè)按照上面建模方式返回的tripselection資源如下:
{ ????"BookingId":?"6cedc5fc-afed-4e34-8906-2ddc4b8cac6f", ????"Booking":?{ ????????"Uri":?"localhost:3000/api/booking/6cedc5fc-afed-4e34-8906-2ddc4b8cac6f" ????}, ????"ChangeFlight":?{ ????????"BookingId":?"6cedc5fc-afed-4e34-8906-2ddc4b8cac6f", ????????"Journey":?{ ????????????"Id":?"00000000-0000-0000-0000-000000000000", ????????????//?Ignore?other?fields ????????}, ????????"Flight":?{ ????????????"Number":?null, ????????????//?Ignore?other?fields ????????}, ????????"PostUrl":?{ ????????????"Uri":? ????????????"localhost:3000/api/booking/6cedc5fc-afed-4e34-8906-2ddc4b8cac6f/flightchange" ????????} ????}, ????"AssignSeat":?{ ????????"BookingId":?"6cedc5fc-afed-4e34-8906-2ddc4b8cac6f", ????????"Seat":?{ ????????????"Number":?null, ????????????"SeatType":?0 ????????}, ????????"Passenger":?{ ????????????"Name":?null, ????????????"PassengerType":?0, ????????????"Age":?0, ????????????"Email":?null ????????}, ????????"PostUrl":?{ ????????????"Uri":? ????????????"localhost:3000/api/booking/6cedc5fc-afed-4e34-8906-2ddc4b8cac6f/seatassignment" ????????} ????} }
也許這種設(shè)計(jì)方式無法滿足所有的場景,但是他可以在一定程度上幫助你創(chuàng)建出更具表達(dá)力的API,同時(shí)也使API消費(fèi)端在一定程度上減少對(duì)文檔的依賴。比起理論,本文更多在討論實(shí)踐及實(shí)現(xiàn)細(xì)節(jié),當(dāng)然很多內(nèi)容難免有紕漏,歡迎大家指正。
API
版權(quán)聲明:本文內(nèi)容由網(wǎng)絡(luò)用戶投稿,版權(quán)歸原作者所有,本站不擁有其著作權(quán),亦不承擔(dān)相應(yīng)法律責(zé)任。如果您發(fā)現(xiàn)本站中有涉嫌抄襲或描述失實(shí)的內(nèi)容,請(qǐng)聯(lián)系我們jiasou666@gmail.com 處理,核實(shí)后本網(wǎng)站將在24小時(shí)內(nèi)刪除侵權(quán)內(nèi)容。