在现实世界中,我们会遇到各种复杂的场景,没有一种API设计方法可以处理所有的场景。与“消费者驱动契约”不同,本文将描述另一种设计API的方式:领域驱动API。这不是API设计的标准方法,但也许可以给你启发,帮助你设计出更有表现力的API。

上图是一个API文档片段。他们通过HTTP操作和统一资源标识符来描述他们的意图。也许他们需要一个好的文档来描述他们的参数和返回类型,消费者可以调用和使用。市面上也有类似Swager这样的高效产品,使用起来也非常方便。然而,这种应用编程接口有一些小的设计问题:

1.无法通过应用编程接口描述上下文

即使描述API资源的HTTP动词和名词基本上可以描述它们的意图,但在使用过程中,一个API文档似乎是不可或缺的。这几年我改掉了评论代码的坏习惯,因为我意识到好的组织结构和代码都是自描述的。但是我们在设计API的时候,大家都接受了写文档的事实。在“消费者驱动契约”的过程中,应该编写一个契约测试来驱动服务器,以保证契约的一致性。API资源有没有可能包含这个合同,有没有可能让消费者遵守?

2.API消费者知道的太多

在上面的API文档片段中,你知道什么时候调用下面的API吗?

你可能不知道,可能是用户下单的时候,或者是用户买单的时候,看需求。这看似合理,但这种情况表明,某些域逻辑有被转移到消费者端的嫌疑。比如你去餐厅吃饭,服务员带了菜单。当你点一份汤时,服务员告诉你,这份菜单有自己的规则。只有先点一份鸡蛋炒饭才能点这个汤。这时候你只有一个选择,就是记住这个规则,下次先点蛋炒饭。有没有可能不把这个规则强加给消费者?

3.脆弱的设计

API通过提供URIs来提供服务,URIs本质上是字符串。作为一个强类型玩家,我不希望这样的弦散落在每个角落。想象一下,我重命名了一个URI,我必须搜索和修改所有使用这个资源的代码。

一、设计领域模型

当我们实践领域驱动设计时,我们在做什么?找出领域边界,根据领域能力抽象设计好模型。通过领域模型提供业务需求的过程就是改变领域模型状态的过程。

同理,我们设计API是为了什么目的?希望我的API不仅能加、删、改、查,还能更有表现力。每一个API都不是独立存在的,它是领域模型在某一时刻的状态和能力的体现。每个API资源都可以告诉消费者当前的域模型有哪些能力,消费者接下来可以做什么,也就是消费者可以请求哪些API资源。

这样,API的设计与领域模型能力的设计紧密相关。我决定以杭空公司的售票业务为例。

业务要求:

一个叫做RestAirline的航空公司提供在线机票出售业务,用户可以按照搜索条件搜索到所有可用的航班当乘客选中一条可用的航班就开始了整个预定流程一旦乘客选择了一条可用的航班就可以修改航班和选择座位当乘客选择完座位还可以添加一些额外的服务,如:接送机服务等, 最后通过不同的支付方式完成支付乘客在飞机起飞前,还可以做在线登机手续并打印登机牌,在Checkin的过程中还可以重新选择座位

注意:括号中的英文术语可以理解为公司的领域术语,我们在进行领域建模时会用到相同的术语,这样可以降低与领域专家的沟通成本。

根据以上要求,我们可以轻松分析几个领域:预订、支付、旅行可用性

1.设计预订域模型

我们以预订领域模型为例来描述设计过程。下面的交互图清楚地描述了预订的功能:

2.实现预订域

实现过程也相当简单。如果您阅读下面的代码,它几乎完全符合前面描述的业务需求。预订域模型的实现需要注意以下几点:

所有属性都是private set,意味着领域模型内部属性是靠自己维护的;AirportTransfer为Maybe类型,意味着在一个完整的Booking中,可以不选择接送机服务;对于Trip属性而言,即便从语言层面上来讲他是引用类型,可以为null,但是一个包含空Trip的Booking是不存在的,所以一个完整的Booking领域模型中,一旦一个非Maybe类型的属性为null,那我们就可以认为这个Booking就是无效的;该类的构造函数被修饰为private,意味着Booking领域模型只能通过选择可用的航班来创建,代码的含义诠释了业务需求;

二、设计具有Domain能力的API

根据上面设计的领域模型,我们可以很容易地设计出第一个表达领域能力的API API:trip:

实际上,这个API的实现是直接调用相应的域模型能力:

站在领域模型的角度,这一能力创建了一个Booking,同时还将一个可用的航班和乘客列表添加到了Booking领域模型中,此时的Booking就拥有了一些初始状态,同时还具备了一定的能力:分配座位和修改航班。站在API消费者的角度,在消费者消费完毕trip这个API之后,除了能够得到一些必要的返回值,还拥有了调用下面三个API的能力:

这三个应用编程接口目前与预订域模型的功能一致。超媒体API的思想是,API资源不仅可以包含必要的返回值,还可以告诉API消费者下一个领域模型的能力和此时领域模型的状态,即API消费者接下来可以请求什么API。

三、实现Hypermedia API

根据以上分析,我们尝试在第一个版本中对trip API返回的资源进行建模,一个初始版本如下:

其中BookingResource、FlightChange和SeatAssignment是对应的API URI地址,以及ASP提供的url helper.action 方法。网络应用编程接口用于生成一个网址。这样的方法接受两个字符串来生成一个url地址,但这不是一个强类型的玩法,所以我立刻想到了通过解析表达式树来生成URIs,并在IUrlHelper上扩展一个方法,使代码更容易支持重构。

理论上所有的API都可以分为两类,Command和Query,其中可以改变领域模型状态的API可以看作是发送命令的API消费者;还有一种API可以分为Query,无论API消费者请求多少次都不会改变领域模型的状态,通常指Get请求。

根据TripResource中包含的三个API,我们也可以将它们分为两类:

查询类的API抽象为链接类型,命令类的API为ChangeFlightCommand。根据上述建模方法返回的行程资源如下:

这个资源包含服务器的返回值BookingId,也返回API消费者接下来可以使用的API列表,其中Command类型的API也包含约定内容。

四、 如何优雅的消费Hypermedia API

根据本文提供的设计思路,由于我们设计的API总是可以返回下一个可用的API列表,所以我们可以认为整个API列表是分层的,服务器只需要向消费者提供一个顶级的API URI。试想一个消费者怎么能消费这样的API。

在第一轮中,应用编程接口消费者必须已经获得了顶级应用编程接口地址,我们希望消费者通过该应用编程接口获得一些有用的信息:

在第二轮中,从之前的资源中获取搜索可用航班的API地址,并根据合同发送请求:

在第三轮中,从上述资源中获取“选择可用航班”的API地址,并根据合同发送请求:

以上是API消费者的C#版本,restAirlineApiNavigator是一个强类型的ApiNavigator,具有以下接口:

当然,如果你的API消费者是Java,你应该不能够写这样的API导航器来帮助你进行类型保证,但是你可以写一个类型版本的API导航器。典型的超媒体消费流程如下:

从领域建模入手,描述超媒体API的创建、实现和消费过程。也许这种设计方法不能满足所有的场景,但是一定程度上可以帮助你创建一个更有表现力的API,同时一定程度上减少API消费者对文档的依赖。

文本/思想工作张洋

欲知更多精彩见解,请关注微信微信官方账号:ThoughtWorks Insights

1.《driven 使用Domain-Driven创建Hypermedia API》援引自互联网,旨在传递更多网络信息知识,仅代表作者本人观点,与本网站无关,侵删请联系页脚下方联系方式。

2.《driven 使用Domain-Driven创建Hypermedia API》仅供读者参考,本网站未对该内容进行证实,对其原创性、真实性、完整性、及时性不作任何保证。

3.文章转载时请保留本站内容来源地址,https://www.lu-xu.com/keji/1671077.html