使用Domain-Driven創(chuàng)建Hypermedia API

      網(wǎng)友投稿 785 2022-05-29

      在現(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è)完整的Booking中,可以不選擇接送機(jī)服務(wù)(TransferService);反之對(duì)于Trip屬性而言,即便從語言層面上來講他也是引用類型,也可以為null,但是一個(gè)包含空Trip的Booking是不存在的,所以一個(gè)完整的Domain里,一旦一個(gè)非`Maybe`類型的屬性為null,那我們就可以認(rèn)為這個(gè)Booking就是無效的;

      該類的構(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?Passengers?=>?_passengers.AsReadOnly(); ????public?Trip?Trip?{?get;?} ????public?IReadOnlyList>?Seats?=>? ????????????????_passengers.Select(p?=>?p.SelectedSeat).ToList().AsReadOnly(); ????public?Maybe?AirportTransfer?{?get;?private?set;?} ????private?readonly?List?_passengers; ????private?readonly?CheckinProcess?_checkinProcess; ????private?Booking(Trip?trip,?List?passengers) ????{ ????????Id?=?Guid.NewGuid(); ????????_checkinProcess?=?CheckinProcess.CreateCheckinProcess(this); ????????Trip?=?trip; ????????_passengers?=?passengers; ????} ????public?static?Booking?SelectTrip(Trip?trip,?List?passengers) ????{ ????????//Validation?for?trip?and?passengers?in?here ????????var?booking?=?new?Booking(trip,?passengers); ????????return?booking; ????} ????public?void?ChangeFlight(Trip.Journey?journey,?Flight?flight) ????{ ????????//?Checking?is?it?eligible?for?changing?flight; ????????Trip.ChangeFlight(journey.Id,?flight); ????} ????public?void?AssignSeat(Seat?seat,?Passenger?passenger) ????{ ????????//Validation?in?here ????????var?p?=?_passengers.Single(s?=>?s.Name.Equals(passenger.Name)); ????????p.AssignSeat(seat); ????} ????//...?Other?capabilities? }

      二、 設(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)求。

      使用Domain-Driven創(chuàng)建Hypermedia API

      針對(duì)TripSelectionResource包含的三個(gè)API,我們也可以將其劃分為兩類:

      public?class?TripSelectionResource { ????private?readonly?IUrlHelper?_urlHelper; ????public?TripSelectionResource(IUrlHelper?urlHelper) ????{ ????????_urlHelper?=?urlHelper; ????} ????public?Guid?BookingId?{?get;?set;?} ????public?Link?Booking?=>? ????????????_urlHelper.Link((BookingController?c)?=>?c.GetBooking(BookingId)); ????public?ChangeFlightCommand?ChangeFlight?=>?new?ChangeFlightCommand(_urlHelper); ????public?AssignSeatCommand?AssignSeat?=>?new?AssignSeatCommand(_urlHelper); }

      一個(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)容。

      上一篇:基于javaweb(springboot)汽車配件管理系統(tǒng)設(shè)計(jì)和實(shí)現(xiàn)以及文檔報(bào)告
      下一篇:MongoDB 第14章 Replica Sets 架構(gòu)(成員深入理解)
      相關(guān)文章
      中文字幕精品亚洲无线码二区| 亚洲香蕉网久久综合影视| 亚洲AV无码专区电影在线观看| 精品国产亚洲男女在线线电影 | 亚洲?v女人的天堂在线观看| 亚洲成A人片在线播放器| 激情内射亚洲一区二区三区爱妻| 亚洲欧洲日韩国产综合在线二区| 久久久久亚洲精品无码系列| 亚洲AV无码久久精品色欲| 国产亚洲成av人片在线观看| 亚洲国产精品VA在线看黑人 | 亚洲啪AV永久无码精品放毛片| 色偷偷女男人的天堂亚洲网| 7777久久亚洲中文字幕| 亚洲妇女熟BBW| 亚洲国产精品无码久久九九大片 | 亚洲中文字幕乱码一区| 亚洲精品无码人妻无码| 亚洲av日韩精品久久久久久a| 看亚洲a级一级毛片| 亚洲成AV人网址| 中文字幕无码精品亚洲资源网| 中文字幕中韩乱码亚洲大片| 国产亚洲精品精华液| 亚洲人成网站在线播放影院在线| 亚洲精品不卡视频| 亚洲AV一二三区成人影片| 亚洲熟妇无码av另类vr影视| 久久亚洲AV成人无码国产最大| 怡红院亚洲红怡院在线观看| 亚洲精品高清在线| 亚洲国产日韩在线视频| 亚洲AV美女一区二区三区| 亚洲最大黄色网站| 亚洲日本成本人观看| 国产精品亚洲一区二区三区在线观看| 男人的天堂亚洲一区二区三区| 亚洲AV中文无码乱人伦| 亚洲中文字幕在线乱码| 亚洲AV日韩精品久久久久久|