使用Domain-Driven创建Hypermedia API
在现实世界中我们会遇到各种各样的复杂场景,没有一种API设计方式可以应对所有的场景。区别于”Consumer-Driven Contract”,本文将描述另外一种设计API的方式:Domain-Driven API。这不是API设计的标准方法,但是也许他可以给你灵感,帮助你设计出更具有表达力的API。
POST /api/customer
POST /api/customer/order
PUT /api/customer
POST /api/customer/notification
上图是一个API文档片段,他们通过HTTP动作加上统一资源标识符(URI)来描述自己的意图,也许还需要一份不错的文档来描述他的参数,返回类型等,就能被消费端调用和使用。市面上也有类似Swager这样高效的产品,用起来也很方便。但是这样的API或多或少有一些设计方面的小问题:
1. 无法通过API描述上下文
纵然HTTP动词加上描述API资源的名词基本能够描述其意图,但是在使用过程中,一份API文档似乎还是少不了。在过去的若干年里,我去掉了给代码写注释的坏毛病,因为我认识到良好的组织结构和代码是自描述的。然而当我们设计API的时候,大家不约而同的接受了编写文档的事实。在”Consumer-Driven Contract”过程中还要编写一份契约测试来驱动服务端保证契约的一致性。有没有可能让API资源包含这一份契约,同时让消费者去遵守契约呢?
2. API消费端知道的太多
在上面的API文档片段中,你知道应该在什么时候调用下面的API吗?
POST /api/customer/notification
你可能不知道,也许是当用户下了订单,也或者是用户支付了订单,这取决于需求。似乎看起来合情合理,但是这样的场景预示着一部分领域逻辑有转移到消费端的嫌疑。打个比方,你去饭店吃饭,服务员拿来了一个菜单,当你点了一份汤的时候,服务员告诉你这个菜单有自己的规则,只有你先点一份蛋炒饭,你才能够点这份汤。这时候你只有一种选择,那就是记住这个规则,下次先点蛋炒饭。有没有可能不要把这个规则强加在消费端呢?
3. 易碎的设计
API以提供URI的方式来提供服务,而URI在本质上就是一个字符串,作为一个强类型玩家,我不希望这样的字符串分散在各个角落,试想我重命名了一个URI,我不得不搜索并修改所有曾经使用过这个资源的代码。
一、设计领域模型
我们在实践领域驱动设计时我们在做什么?找出领域边界,根据领域的能力做出抽象并设计良好的模型。而领域模型在提供业务需求的过程就是领域模型状态发生变化的过程。
同样的道理,我们设计API是为了达到什么目的?我希望我的API不但能够完成增删改查,还能够更具表达力。每一个API不是独立存在的,他们是领域模型在某一时刻状态和能力的体现,每一个API资源在告知消费者目前领域模型状态的同时,还可以告诉消费者当前领域模型具备了什么样的能力,消费者接下来能够做什么,也即消费者能够请求哪一个API资源。
这么说来API的设计实际上跟领域模型能力的设计有千丝万缕的关系,我决定用航空公司的卖票业务来举例说明。
业务需求:
- 一个叫做RestAirline的航空公司提供在线机票出售业务,用户可以按照搜索条件搜索到所有可用的航班(trip)
- 当乘客选中一条可用的航班(trip)就开始了整个预定(booking)流程
- 一旦乘客选择了一条可用的航班就可以修改航班(change trip)和选择座位(seat)
- 当乘客选择完座位还可以添加一些额外的服务,如:接送机服务(transfer service)等, 最后通过不同的支付方式完成支付(payment)
- 乘客在飞机起飞前,还可以做在线登机手续(checkin)并打印登机牌(boardingpass),在Checkin的过程中还可以重新选择座位
注意: 括号中的英文术语可以理解为该公司的领域术语, 我们在领域建模的时候也会使用相同的术语,从而减少跟领域专家的沟通成本。
就上面的需求我们可以很容易的分析出若干个领域: Booking, Payment, Trip Avalability
1. 设计Booking领域模型
我们以Booking领域模型为例来描述设计过程,下面的交互图清晰的描述出了Booking的能力:
2. 实现Booking Domain
实现过程也相当的直接,如果将下面的代码阅读出来,几乎跟之前描述的业务需求是完全匹配的。Booking领域模型的实现需要注意下面几点:
- 所有属性都是private set,意味着领域模型内部属性是靠自己维护的;
- AirportTransfer为Maybe类型,意味着在一个完整的Booking中,可以不选择接送机服务(TransferService);对于Trip属性而言,即便从语言层面上来讲他是引用类型,可以为null,但是一个包含空Trip的Booking是不存在的,所以一个完整的Booking领域模型中,一旦一个非Maybe类型的属性为null,那我们就可以认为这个Booking就是无效的;
- 该类的构造函数被修饰为private,意味着Booking领域模型只能通过选择可用的航班来创建,代码的含义诠释了业务需求;
public class Booking{public Guid Id { get; }public IReadOnlyList<Passenger> Passengers => _passengers.AsReadOnly();public Trip Trip { get; }public IReadOnlyList<Maybe<Seat>> Seats => _passengers.Select(p => p.SelectedSeat).ToList().AsReadOnly();public Maybe<AirportTransfer> AirportTransfer { get; private set; }private readonly List<Passenger> _passengers;private readonly CheckinProcess _checkinProcess;private Booking(Trip trip, List<Passenger> passengers){Id = Guid.NewGuid();_checkinProcess = CheckinProcess.CreateCheckinProcess(this);Trip = trip;_passengers = passengers;}public static Booking SelectTrip(Trip trip, List<Passenger> passengers){//Validation for trip and passengers in herevar booking = new Booking(trip, passengers);return booking;}public void ChangeFlight(Flight flight){// Checking is it eligible for changing flight;Trip.ChangeFlight(journey.Id, flight);}public void AssignSeat(Seat seat, Passenger passenger){//Validation in herevar p = _passengers.Single(s => s.Name.Equals(passenger.Name));p.AssignSeat(seat);}//... Other capabilities }
二、设计具有Domain能力的API
根据上面设计好的领域模型,我们可以轻松设计出第一个表达领域能力的API: trip:
POST /api/booking/trip
实际上这一API的实现方式就是直接调用对应的领域模型能力:
var booking = Booking.SelectTrip(trip, passengers)
- 站在领域模型的角度,这一能力创建了一个Booking,同时还将一个可用的航班(Trip)和乘客列表添加到了Booking领域模型中,
此时的Booking就拥有了一些初始状态,同时还具备了一定的能力:分配座位(seat)和修改航班(flight)。 - 站在API消费者的角度,在消费者消费完毕trip这个API之后,除了能够得到一些必要的返回值,还拥有了调用下面三个API的能力:
GET api/booking/{id}
PUT api/booking/{id}/seat
PUT api/booking/{id}/flight
这三个API跟Booking领域模型在此时拥有的能力是一致的。Hypermedia API的思想在于:API资源除了包含必要的返回值,还能告诉API消费者下一步领域模型拥有的能力和此时领域模型的状态,也就是API消费者接下来可以请求什么样的API。
三、实现Hypermedia API
根据上面的分析,我们尝试对trip API返回的资源进行第一版建模,一个最初的版本如下:
public class TripResource{private readonly IUrlHelper _urlHelper;public TripResource(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,FlightChange,SeatAssignment 为对应的API URI地址,使用了ASP.NET Web API提供的 urlHelper.Action(“ActionName”,”ControllerName”) 方法来生成一个url。这样的一个方法接受两个字符串来生成一个url地址,但这并不是强类型的玩法,所以马上想到通过解析表达式树的方式生成URI,在IUrlHelper上扩展一个方法,使得代码更容易支持重构。
public class TripResource{private readonly IUrlHelper _urlHelper;public TripResource(IUrlHelper urlHelper){_urlHelper = urlHelper;}public Guid BookingId { get; set; }public string BookingResource => _urlHelper.Link((BookingController c) => c.GetBooking(BookingId));public string FlightChange => _urlHelper.Link((BookingController c) => c.ChangeFlight());public string SeatAssignment => _urlHelper.Link((BookingController c) => c.AssignSeat());}
理论上所有的API都能划分为两类,Command和Query(参考CQRS pattern),其中能够改变领域模型状态的API都可以认为是API消费者发送了一个Command;另一类API则可以划分到Query,无论API消费者请求多少遍都不会改变领域模型的状态,通常指Get请求。
针对TripResource包含的三个API,我们也可以将其划分为两类:
public class TripResource{private readonly IUrlHelper _urlHelper;public Trip(IUrlHelper urlHelper){_urlHelper = urlHelper;}public Guid BookingId { get; set; }public Link<BookingResource> Booking => _urlHelper.Link((BookingController c) => c.GetBooking(BookingId));public ChangeFlightCommand ChangeFlight => new ChangeFlightCommand(_urlHelper);public AssignSeatCommand AssignSeat => new AssignSeatCommand(_urlHelper);}
Query类的API被抽象为Link类型,Command类的API如 ChangeFlightCommand。一个按照上面建模方式返回的trip资源如下:
{"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"}}}
这一份资源包含了服务端返回值BookingId, 同时还返回了此时API消费端接下来能够使用的API列表,其中Command类型的API还包含了契约内容。
四、 如何优雅的消费Hypermedia API
按照本文提供的设计思路,因为我们设计好的API总能够返回下次可用的API列表,所以我们可以认为整个API列表是有层级关系的,服务端只需要提供一个最顶端的API URI给消费者即可。试想一个消费端如何消费这样的API呢?
第一个回合,一定是API消费端拿到了最顶端的API地址,我们期望消费端能够通过这个API得到一些有用的信息:
var homeResource = restAirlineApiNavigator.Execute();
第二个回合,从上一个资源中拿到搜索可用航班的API地址,按照契约发送请求:
var searchTripsCommand = homeResource.SearchTripsCommand;searchTripsCommand.SearchCriteria = TripSearchCriteria.DefaultTripSearchCriteria();var tripAvailabilityResource = restAirlineApiNavigator.PostCommand(searchTripsCommand);
第三个回合,从上面的资源中拿到”选择可用航班”的API地址,按照契约发送请求:
var selectTripCommand = tripAvailabilityResource.SelectTripCommand;selectTripCommand.Trip = tripAvailabilityResource.AvailableTrips.First();var tripResource = restAirlineApiNavigator.PostCommand(selectTripCommand);
上面是一个C#版本的API消费端,restAirlineApiNavigator是一个强类型API Navigator,他拥有下面接口:
public interface IApiNavigator<TResource>{TResource Execute();TResourceToFetch PostCommand<TResourceToFetch>(HypermediaCommand<TResourceToFetch> command);SubApiNavigator<TTargetResource, TResource> FollowLink<TTargetResource>(Func<TResource, Link<TTargetResource>> navigator);}
当然,如果你API消费端是Javascript,你应该没法写出这样的API Navigator来帮你做类型保证,不过你可以写一个TypeScript版本的API navigator,一个典型的Hypermedia消费过程如下:
getProducts(): Observable<ProductsResource> {const products = this.apiNavigator.followLink(start => start.productHome).followLink(product => product.products).execute();return products;}
本文从领域建模出发,描述了Hypermedia API的创建、实现以及消费过程,也许这种设计方式无法满足所有的场景,但是他可以在一定程度上帮助你创建出更具表达力的API,同时也使API消费端在一定程度上减少对文档的依赖。
文/ThoughtWorks张阳
更多精彩洞见,请关注微信公众号:ThoughtWorks洞见
使用Domain-Driven创建Hypermedia API相关推荐
- 初探领域驱动设计(Domain Driven Design)
前言: 我个人在学习DDD的过程中,早期翻找各种资料的时候,看到了很多名词:战略设计.战术设计.聚合根.实体.值对象.界限上下文...这些繁多的名词定义配合上几乎少的可怜的实战例子,让我在翻阅了大量资 ...
- DDD(Domain Driven Design) 领域驱动设计从理论到实践 四
- 接上 SOA 架构 面向服务架构(Service Oriented Architecture,SOA)对于不同的人来说意思不同.这里梳理一下SOA原则: 服务契约 : 通过契约文档,服 ...
- 实施领域驱动设计(Implementing Domain Driven Design翻译)
实施领域驱动设计(Implementing Domain Driven Design翻译) 引言 介绍 这是实现领域驱动的实用指南设计(DDD).虽然实现细节依赖于ABP 框架基础设施,但是核心概念. ...
- [译文]Domain Driven Design Reference(四)—— 柔性设计
本书是Eric Evans对他自己写的<领域驱动设计-软件核心复杂性应对之道>的一本字典式的参考书,可用于快速查找<领域驱动设计>中的诸多概念及其简明解释. 其它本系列其它文章 ...
- [译文]Domain Driven Design Reference(五)—— 为战略设计的上下文映
本书是Eric Evans对他自己写的<领域驱动设计-软件核心复杂性应对之道>的一本字典式的参考书,可用于快速查找<领域驱动设计>中的诸多概念及其简明解释. 其它本系列其它文章 ...
- [译文]Domain Driven Design Reference(三)—— 模型驱动设计的构建模块
本书是Eric Evans对他自己写的<领域驱动设计-软件核心复杂性应对之道>的一本字典式的参考书,可用于快速查找<领域驱动设计>中的诸多概念及其简明解释. 其它本系列其它文章 ...
- 元数据驱动设计 —— 为动态移动应用创建Web API
时间回到多年之前(当时我的头发还没这么稀疏),Google在4月1日这一天发布了Gmail,这不由得令许多人怀疑这个产品是否只是Google精心炮制的一个玩笑.但谁又能够去指责他们的怀疑呢?毕竟整个互 ...
- Domain Driven Design
在Spring官网的第一个tutorial中看到了这种 设计模式 Domain Driven Design 找到了篇介绍这个得文章: What is Domain Driven Design? &qu ...
- Domain Driven Design and Development In Practice--转载
原文地址:http://www.infoq.com/articles/ddd-in-practice Background Domain Driven Design (DDD) is about ma ...
- 在ASP.NET Core 2.0中创建Web API
目录 介绍 先决条件 软件 技能 使用代码 第01步 - 创建项目 第02步 - 安装Nuget包 步骤03 - 添加模型 步骤04 - 添加控制器 步骤05 - 设置依赖注入 步骤06 - 运行We ...
最新文章
- 计算机基础知识_2020年河北省高职单招计算机基础知识和实践技能培训
- Github上的十大机器学习项目
- js断点和调试学习总结3
- Spring Data对Cassandra 3的支持
- php教程知识点归纳,PHP知识点小结
- Docker Windows 安装
- 今天为你分享互联网营销的两个核心思维
- 踢掉 Docker 后,Kubernetes 还能欢快地跑 GPU?
- 堆排序matlab,matlab 堆排序 ...原创(初来报到)
- vue学习笔记-1-初步认识
- 服务器搬迁清单需要启动任务以及恢复办法
- excel制作复合饼状图_如何在Excel中制作饼图
- Privates下载
- You need to prove you’re evil cheap nike air max
- 外汇短线交易者的规则
- 信用卡还款怎么分期,还款还是要技巧的
- 实验六 文本串的加密解密
- 锐龙R5 4500 怎么样 相当于什么水平
- 【维修】如何成功做网线?
- 作为一个UI设计师的3个基本素养,你具备哪些?
热门文章
- 有道云笔记同步失败原因之一
- oracle analyze失效,ORACLE: Analyze Table 失敗
- 送给女朋友的圣诞节电子贺卡源代码,圣诞节快乐代码
- 计算机毕业设计asp.net193酒店客房预订网站系统
- 「目标检测算法」连连看:从Faster R-CNN 、 R-FCN 到 FPN
- 空战神作《浴血长空》折扣充值平台全方位多角度体验
- 你的才艺怎样变现?--Rarible平台
- 让Myeclipse10支持Mac OSX – Retina显示屏
- python语句分号_你知道分号在各种编程语言中的作用吗?
- c语言char a什么意思,C语言中char *a[ ]什么意思,他和char (*)a[ ]有什么什么区别?...