使用DDD来构建你的REST API,而不是CRUD

DD的博客全面升级,阅读体验更佳(尤其是系列教程),后续不再通过这里发布新文章,而是改到 www.didispace.com 发布啦,奔走相告!点击直达~

REST围绕着资源这个概念而构建的,然后用URI来表示。然后一个HTTP动词和资源URI组合起来对指定资源进行HTTP调用来执行操作。大多数REST框架提供了指定资源名称的生成器,框架围绕着它来生成脚手架。不幸的是,许多这些生成器使用CRUD模型(Create,Read, Update, Delete)作为默认的起始点。资源被定义为一系列的属性,使用类似JSON Schema或某个具体语言的数据对象来定义,然后生成方法存根,然后来创建,读取,更新和删除该资源。

尽管这可以让开发人员觉得理解和开始工作变得简单了许多,是一个很好的起点,但是使用CRUD作为API的起点,我有一个很大的疑问。就是CRUD中的U是我最不喜欢的。让我们来谈谈U.通用更新方法允许客户端更新资源的任何字段,然后使用新版本覆盖现有版本。但是,如果允许客户端执行这样的操作,您的服务API在其使用的任何底层数据存储之上,所能提供的价值其实是很小的。服务层的关键增值之一就是在基础数据之上实施业务约束,资源总是最终要被业务约束才行。

难道我们就不能添加业务约束到我们的更新方法上吗?我们以简单的银行帐户资源为例,看看会发生什么。首先,客户端不应该调用一个API,然后就把账户余额更新为他们想要的数量,这不是乱套了吗?!帐户可能有最低余额。 ok,于是你对那些更新方法添加了一些校验代码,以便如果帐户余额值被更改,它必须在一个指定的范围内。这样问题解决了吗?没有。任何余额调整都应被作为某种类型交易事务被记录下来才对。比如这是充值?取钱?还是一次转账?如果客户端尝试更改帐号怎么办?这是否允许?会破坏其他数据关系吗?于是你的更新(update)方法实现逻辑将会快速变成了意大利面条代码(就是逻辑流程搞得异常复杂的代码)。我已经发现一些团队就是这样做的,他们的代码试图推断客户端究竟把哪些字段改变了,代码最终就是一团糟。

那有什么办法呢?就个人而言,我是领域驱动设计(DDD)(设计任何类型的API)的超级粉丝。 DDD的思路是希望软件建模应该是基于解决现实世界的问题而去设计API。它创建了一种用于描述软件的语言,这种语言是基于被称为实体或聚合的关键的业务对象来描述软件的。它还定义了比如服务(Services),值对象(ValueObject)和存储库(Repositories)之类的术语,它们共同解决特定业务领域中的问题,或者在DDD术语中被叫做“有界上下文(Bounded Context)”。当然,并不是说你必须使用DDD来设计你的REST,但是,由于REST资源可以很好地映射到DDD实体,因此我发现设计REST API特别适合使用DDD。

那么这是什么意思?这意味着你的**API应该**围绕领域对象及其提供的业务操作。业务操作是通用更新方法及其所有陷阱的关键的替代方案。让我们用前面的银行示例来说明。

对于银行API,明显的领域对象(或DDD术语中的实体)是一个帐户,它为银行帐户建模。我们不应该按照帐户的CRUD模型来定义在银行账户上执行的具体业务操作。以下是一个写操作系列很好的开始:

  1. Open -开户
  2. Close -关闭账户
  3. Debit -从账户上取钱
  4. Credit -往账户上加钱

这些操作是具体的,可以强制执行某些业务约束。例如,我们可能不想允许记入已关闭的账户,我们可以强制执行我们的最低余额检查作为借记操作的一部分。在读操作方面,我们还可以提供与我们的客户用例相匹配的特定查询:

  1. Load -通过其帐户ID加载单个帐户。
  2. Transaction history - 列出帐户的交易记录。
  3. Customer accounts -列出给定客户ID的帐户。

现在我们知道我们的业务操作是什么了,下面是将它们映射到REST API的一个例子:

  1. POST /account – 开户
  2. PUT /account/<accountId>/close -关闭现有账户
  3. PUT /account/<accountId>/debit – 从账户上取钱
  4. PUT /account/<accountId>/credit – 往账户上充钱
  5. GET /account/<acountId> - 通过其帐户ID加载单个帐户。
  6. GET /account/<accountId>/transactions- 列出帐户的交易记录。
  7. GET /accounts/query/customerId/<customerId> -列出给定客户ID的帐户。

这看起来和基本的CRUD API有很大的不同,但关键是允许的操作是特定的和明确的。这为服务实现者以及客户端带来了更好的体验。服务实现不再需要基于哪些属性更新来猜测什么业务操作是隐含的。相反,业务操作是明确的,这样我们的代码实现也更简单,更可维护。在客户端,将变得更加的明确,什么操作可以执行,什么操作不可以执行。如果API文档记录的很好的话,例如使用Swagger来定义文档,那么每个API的限制(或约束)将变得非常明确。

以这种方式定义你的API需要更多的前瞻性思考,要比简单的CRUD 生成器需要花费更多的思考,但我认为这是值得的也是必须的。如果你计划将API作为公共端点来公开,那么你就必须在非常长的时间内支持该API。基本上认为它是软件标准的永远。我总是鼓励团队在以后难以改变的事情上花时间,API就是这样的例子。

因此不应该按照CRUD模型来构建你的serviceAPI(REST 或其他),而应该是使用DDD,DDD可以根据领域对象和可对其执行的业务操作来定义API。

希望本文对你有帮助!

本文作者:Hood著 贺卓凡译,
原文链接:https://mp.weixin.qq.com/s/251ql2WhDi-InUgVtIQ6_Q
版权归作者所有,转载请注明作者、原文、译者等出处信息