在現(xiàn)實(shí)世界中,我們會(huì)遇到各種復(fù)雜的場(chǎng)景,沒有一種API設(shè)計(jì)方法可以處理所有的場(chǎng)景。與“消費(fèi)者驅(qū)動(dòng)契約”不同,本文將描述另一種設(shè)計(jì)API的方式:領(lǐng)域驅(qū)動(dòng)API。這不是API設(shè)計(jì)的標(biāo)準(zhǔn)方法,但也許可以給你啟發(fā),幫助你設(shè)計(jì)出更有表現(xiàn)力的API。

上圖是一個(gè)API文檔片段。他們通過(guò)HTTP操作和統(tǒng)一資源標(biāo)識(shí)符來(lái)描述他們的意圖。也許他們需要一個(gè)好的文檔來(lái)描述他們的參數(shù)和返回類型,消費(fèi)者可以調(diào)用和使用。市面上也有類似Swager這樣的高效產(chǎn)品,使用起來(lái)也非常方便。然而,這種應(yīng)用編程接口有一些小的設(shè)計(jì)問(wèn)題:

1.無(wú)法通過(guò)應(yīng)用編程接口描述上下文

即使描述API資源的HTTP動(dòng)詞和名詞基本上可以描述它們的意圖,但在使用過(guò)程中,一個(gè)API文檔似乎是不可或缺的。這幾年我改掉了評(píng)論代碼的壞習(xí)慣,因?yàn)槲乙庾R(shí)到好的組織結(jié)構(gòu)和代碼都是自描述的。但是我們?cè)谠O(shè)計(jì)API的時(shí)候,大家都接受了寫文檔的事實(shí)。在“消費(fèi)者驅(qū)動(dòng)契約”的過(guò)程中,應(yīng)該編寫一個(gè)契約測(cè)試來(lái)驅(qū)動(dòng)服務(wù)器,以保證契約的一致性。API資源有沒有可能包含這個(gè)合同,有沒有可能讓消費(fèi)者遵守?

2.API消費(fèi)者知道的太多

在上面的API文檔片段中,你知道什么時(shí)候調(diào)用下面的API嗎?

你可能不知道,可能是用戶下單的時(shí)候,或者是用戶買單的時(shí)候,看需求。這看似合理,但這種情況表明,某些域邏輯有被轉(zhuǎn)移到消費(fèi)者端的嫌疑。比如你去餐廳吃飯,服務(wù)員帶了菜單。當(dāng)你點(diǎn)一份湯時(shí),服務(wù)員告訴你,這份菜單有自己的規(guī)則。只有先點(diǎn)一份雞蛋炒飯才能點(diǎn)這個(gè)湯。這時(shí)候你只有一個(gè)選擇,就是記住這個(gè)規(guī)則,下次先點(diǎn)蛋炒飯。有沒有可能不把這個(gè)規(guī)則強(qiáng)加給消費(fèi)者?

3.脆弱的設(shè)計(jì)

API通過(guò)提供URIs來(lái)提供服務(wù),URIs本質(zhì)上是字符串。作為一個(gè)強(qiáng)類型玩家,我不希望這樣的弦散落在每個(gè)角落。想象一下,我重命名了一個(gè)URI,我必須搜索和修改所有使用這個(gè)資源的代碼。

一、設(shè)計(jì)領(lǐng)域模型

當(dāng)我們實(shí)踐領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)時(shí),我們?cè)谧鍪裁矗空页鲱I(lǐng)域邊界,根據(jù)領(lǐng)域能力抽象設(shè)計(jì)好模型。通過(guò)領(lǐng)域模型提供業(yè)務(wù)需求的過(guò)程就是改變領(lǐng)域模型狀態(tài)的過(guò)程。

同理,我們?cè)O(shè)計(jì)API是為了什么目的?希望我的API不僅能加、刪、改、查,還能更有表現(xiàn)力。每一個(gè)API都不是獨(dú)立存在的,它是領(lǐng)域模型在某一時(shí)刻的狀態(tài)和能力的體現(xiàn)。每個(gè)API資源都可以告訴消費(fèi)者當(dāng)前的域模型有哪些能力,消費(fèi)者接下來(lái)可以做什么,也就是消費(fèi)者可以請(qǐng)求哪些API資源。

這樣,API的設(shè)計(jì)與領(lǐng)域模型能力的設(shè)計(jì)緊密相關(guān)。我決定以杭空公司的售票業(yè)務(wù)為例。

業(yè)務(wù)要求:

一個(gè)叫做RestAirline的航空公司提供在線機(jī)票出售業(yè)務(wù),用戶可以按照搜索條件搜索到所有可用的航班當(dāng)乘客選中一條可用的航班就開始了整個(gè)預(yù)定流程一旦乘客選擇了一條可用的航班就可以修改航班和選擇座位當(dāng)乘客選擇完座位還可以添加一些額外的服務(wù),如:接送機(jī)服務(wù)等, 最后通過(guò)不同的支付方式完成支付乘客在飛機(jī)起飛前,還可以做在線登機(jī)手續(xù)并打印登機(jī)牌,在Checkin的過(guò)程中還可以重新選擇座位

注意:括號(hào)中的英文術(shù)語(yǔ)可以理解為公司的領(lǐng)域術(shù)語(yǔ),我們?cè)谶M(jìn)行領(lǐng)域建模時(shí)會(huì)用到相同的術(shù)語(yǔ),這樣可以降低與領(lǐng)域?qū)<业臏贤ǔ杀尽?/p>

根據(jù)以上要求,我們可以輕松分析幾個(gè)領(lǐng)域:預(yù)訂、支付、旅行可用性

1.設(shè)計(jì)預(yù)訂域模型

我們以預(yù)訂領(lǐng)域模型為例來(lái)描述設(shè)計(jì)過(guò)程。下面的交互圖清楚地描述了預(yù)訂的功能:

2.實(shí)現(xiàn)預(yù)訂域

實(shí)現(xiàn)過(guò)程也相當(dāng)簡(jiǎn)單。如果您閱讀下面的代碼,它幾乎完全符合前面描述的業(yè)務(wù)需求。預(yù)訂域模型的實(shí)現(xiàn)需要注意以下幾點(diǎn):

所有屬性都是private set,意味著領(lǐng)域模型內(nèi)部屬性是靠自己維護(hù)的;AirportTransfer為Maybe類型,意味著在一個(gè)完整的Booking中,可以不選擇接送機(jī)服務(wù);對(duì)于Trip屬性而言,即便從語(yǔ)言層面上來(lái)講他是引用類型,可以為null,但是一個(gè)包含空Trip的Booking是不存在的,所以一個(gè)完整的Booking領(lǐng)域模型中,一旦一個(gè)非Maybe類型的屬性為null,那我們就可以認(rèn)為這個(gè)Booking就是無(wú)效的;該類的構(gòu)造函數(shù)被修飾為private,意味著Booking領(lǐng)域模型只能通過(guò)選擇可用的航班來(lái)創(chuàng)建,代碼的含義詮釋了業(yè)務(wù)需求;

二、設(shè)計(jì)具有Domain能力的API

根據(jù)上面設(shè)計(jì)的領(lǐng)域模型,我們可以很容易地設(shè)計(jì)出第一個(gè)表達(dá)領(lǐng)域能力的API API:trip:

實(shí)際上,這個(gè)API的實(shí)現(xiàn)是直接調(diào)用相應(yīng)的域模型能力:

站在領(lǐng)域模型的角度,這一能力創(chuàng)建了一個(gè)Booking,同時(shí)還將一個(gè)可用的航班和乘客列表添加到了Booking領(lǐng)域模型中,此時(shí)的Booking就擁有了一些初始狀態(tài),同時(shí)還具備了一定的能力:分配座位和修改航班。站在API消費(fèi)者的角度,在消費(fèi)者消費(fèi)完畢trip這個(gè)API之后,除了能夠得到一些必要的返回值,還擁有了調(diào)用下面三個(gè)API的能力:

這三個(gè)應(yīng)用編程接口目前與預(yù)訂域模型的功能一致。超媒體API的思想是,API資源不僅可以包含必要的返回值,還可以告訴API消費(fèi)者下一個(gè)領(lǐng)域模型的能力和此時(shí)領(lǐng)域模型的狀態(tài),即API消費(fèi)者接下來(lái)可以請(qǐng)求什么API。

三、實(shí)現(xiàn)Hypermedia API

根據(jù)以上分析,我們嘗試在第一個(gè)版本中對(duì)trip API返回的資源進(jìn)行建模,一個(gè)初始版本如下:

其中BookingResource、FlightChange和SeatAssignment是對(duì)應(yīng)的API URI地址,以及ASP提供的url helper.action 方法。網(wǎng)絡(luò)應(yīng)用編程接口用于生成一個(gè)網(wǎng)址。這樣的方法接受兩個(gè)字符串來(lái)生成一個(gè)url地址,但這不是一個(gè)強(qiáng)類型的玩法,所以我立刻想到了通過(guò)解析表達(dá)式樹來(lái)生成URIs,并在IUrlHelper上擴(kuò)展一個(gè)方法,使代碼更容易支持重構(gòu)。

理論上所有的API都可以分為兩類,Command和Query,其中可以改變領(lǐng)域模型狀態(tài)的API可以看作是發(fā)送命令的API消費(fèi)者;還有一種API可以分為Query,無(wú)論API消費(fèi)者請(qǐng)求多少次都不會(huì)改變領(lǐng)域模型的狀態(tài),通常指Get請(qǐng)求。

根據(jù)TripResource中包含的三個(gè)API,我們也可以將它們分為兩類:

查詢類的API抽象為鏈接類型,命令類的API為ChangeFlightCommand。根據(jù)上述建模方法返回的行程資源如下:

這個(gè)資源包含服務(wù)器的返回值BookingId,也返回API消費(fèi)者接下來(lái)可以使用的API列表,其中Command類型的API也包含約定內(nèi)容。

四、 如何優(yōu)雅的消費(fèi)Hypermedia API

根據(jù)本文提供的設(shè)計(jì)思路,由于我們?cè)O(shè)計(jì)的API總是可以返回下一個(gè)可用的API列表,所以我們可以認(rèn)為整個(gè)API列表是分層的,服務(wù)器只需要向消費(fèi)者提供一個(gè)頂級(jí)的API URI。試想一個(gè)消費(fèi)者怎么能消費(fèi)這樣的API。

在第一輪中,應(yīng)用編程接口消費(fèi)者必須已經(jīng)獲得了頂級(jí)應(yīng)用編程接口地址,我們希望消費(fèi)者通過(guò)該應(yīng)用編程接口獲得一些有用的信息:

在第二輪中,從之前的資源中獲取搜索可用航班的API地址,并根據(jù)合同發(fā)送請(qǐng)求:

在第三輪中,從上述資源中獲取“選擇可用航班”的API地址,并根據(jù)合同發(fā)送請(qǐng)求:

以上是API消費(fèi)者的C#版本,restAirlineApiNavigator是一個(gè)強(qiáng)類型的ApiNavigator,具有以下接口:

當(dāng)然,如果你的API消費(fèi)者是Java,你應(yīng)該不能夠?qū)戇@樣的API導(dǎo)航器來(lái)幫助你進(jìn)行類型保證,但是你可以寫一個(gè)類型版本的API導(dǎo)航器。典型的超媒體消費(fèi)流程如下:

從領(lǐng)域建模入手,描述超媒體API的創(chuàng)建、實(shí)現(xiàn)和消費(fèi)過(guò)程。也許這種設(shè)計(jì)方法不能滿足所有的場(chǎng)景,但是一定程度上可以幫助你創(chuàng)建一個(gè)更有表現(xiàn)力的API,同時(shí)一定程度上減少API消費(fèi)者對(duì)文檔的依賴。

文本/思想工作張洋

欲知更多精彩見解,請(qǐng)關(guān)注微信微信官方賬號(hào):ThoughtWorks Insights

1.《driven 使用Domain-Driven創(chuàng)建Hypermedia API》援引自互聯(lián)網(wǎng),旨在傳遞更多網(wǎng)絡(luò)信息知識(shí),僅代表作者本人觀點(diǎn),與本網(wǎng)站無(wú)關(guān),侵刪請(qǐng)聯(lián)系頁(yè)腳下方聯(lián)系方式。

2.《driven 使用Domain-Driven創(chuàng)建Hypermedia API》僅供讀者參考,本網(wǎng)站未對(duì)該內(nèi)容進(jìn)行證實(shí),對(duì)其原創(chuàng)性、真實(shí)性、完整性、及時(shí)性不作任何保證。

3.文章轉(zhuǎn)載時(shí)請(qǐng)保留本站內(nèi)容來(lái)源地址,http://f99ss.com/keji/1671077.html