皇上,还记得我吗?我就是1999年那个Linux伊甸园啊-----24小时滚动更新开源资讯,全年无休!

为什么说要用DDD替代CRUD来设计API

作者 James Hood ,译者 薛命灯

来自亚马逊的高级工程师James Hood以简单明了的例子说明了为什么要用DDD替代CRUD来设计REST API。

REST以资源为中心,这些资源以URI的形式呈现。在调用HTTP时,通过指定一个HTTP动词和一个资源URI对某个特定的资源进行操作。大部分REST框架都提供了生成器,你只要指定一个资源的名字,框架就会为你生成脚手架(scaffold)。不过,这些生成器默认使用的是CRUD模型(Create、Read、Update、Delete),它们把资源看成是一系列属性的集合,使用JSON或与特定语言相关的数据对象来表示资源,并生成用于对资源进行创建、读取、更新和删除操作的方法。

虽然这给开发者带来了便利,但我觉得这样是有问题的。我不喜欢CRUD这样的说法,尤其不喜欢当中的U。一般的更新操作允许客户端更新资源的任何一个字段,并使用新版本覆盖已有的版本。但如果你允许客户端这么做,那么你的服务API就失去了应有的价值。服务层的一个关键价值在于为底层的数据增加业务约束,因此,资源最终都需要带上业务约束。

那么,难道我们就不能给更新操作增加业务约束吗?让我们以最简单的银行账户为例。首先,不能让客户通过调用API来随意更新他们的账户余额。另外,账户或许需要最小余额的限制。你在更新操作里做了一些检查,账户余额的变动必须发生在一个指定的范围内。那么这样问题就解决了吗?当然没有。任何一次余额的调整都需要与某种事务相对应,不是吗?是存入、取出,还是转账?如果客户要更改账户该怎么办?这样做是被允许的吗?这样做会不会破坏与其他数据之间的关系?不难看出,你的更新操作很快会让这一切变得像意大利面条一样混乱不堪。我曾经看着一些团队走上了这条不归路,他们试图从更新的字段里去推测客户的意图,结果代码变得像团乱麻。

那么该如何解决这个问题,有其他更好的方案吗?我个人更喜欢基于领域驱动设计(DDD)来设计API。DDD的基本思想是说,软件的建模应该发生在真实世界的问题得到解决之后。DDD使用实体(Entity)和聚合(Aggregate)来描述业务对象,还定义了服务(Service)、值对象(Value Object)和仓库(Repository)等术语,用以解决业务领域或DDD边界上下文问题。DDD不一定非要与REST绑定在一起,不过我发现DDD与REST API近乎天然地合拍,因为REST的资源可以很好地与DDD的实体映射起来。

那么这意味着什么呢?这意味着,你的API应该要以领域对象以及这些对象所提供的业务操作为中心。业务操作是对常规更新操作最好的替代品。我们继续以之前的银行账户为例。

对于银行的API来说,账户就是一个领域对象(DDD里的实体)。这次我们不再使用CRUD来为账户建模,而是为账户定义一组业务操作。以下是一系列写入操作:

  • 开户(Open)——新开一个账户。
  • 销户(Close)——注销一个已有的账户。
  • 取出(Debit)——从账户里扣掉一些钱。
  • 存入(Credit)——往账户里存入一些钱。

这些操作都带有一定的业务约束。例如,往一个已经注销的账户里存钱是不被允许的,而在取钱的时候要强制检查最小余额。至于读取操作,我们可以为客户提供一些有用的查询。

  • 加载(Load)——通过账户ID加载相应的账户信息。
  • 交易历史——列出账户的交易历史。
  • 客户的账户列表——列出指定客户的所有账户。

在定义好业务操作之后,就可以将它们与REST API映射起来。

  1. POST /account ——新开一个账户。
  2. PUT /account/<accountId>/close ——注销一个已有的账户。
  3. PUT /account/<accountId>/debit ——从账户里扣掉一些钱。
  4. PUT /account/<accountId>/credit ——往账户里存入一些钱。
  5. GET /account/<accountId> ——通过账户ID加载相应的账户信息。
  6. GET /account/<accountId>/transactions ——列出账户的交易历史。
  7. GET /accounts/query/customerId/<customerId> ——列出指定客户的所有账户。

这些看起来与一般的CRUD API非常的不一样,关键在于这些操作具有良好的定义。不管对于服务提供方还是客户端来说,这样的体验都更好。服务提供方不再需要根据更新字段来推测业务操作的意图,业务操作清晰明了,这样的代码更简单,也更容易维护。而对于客户端来说,它们能执行或不能执行哪些操作也是一目了然的。如果API具有良好的文档化,比如使用了Swagger,那么就可以很清楚地了解到API都具有哪些约束。

定义这样的API需要做一些前期思考,这不同于使用简单的CRUD生成器。如果你打算将API暴露成公共端点,就需要在很长的一段时间内为API提供支持,最好还是把它看成是一个永久性的事项。我总是建议人们在前期多花一点时间,因为有些东西到了后面就很难修改,而API就是一个很好的例子。

所以,在进行API(REST或其他)设计时,请停止使用CRUD模型。相反,可以通过DDD来定义API,包括领域对象和它们的业务操作。

如果你想看到更多关于领域对象的例子,可以参考Amazon Web Services的API。在AWS API开发者指南里,每一个服务都有对应的“关键概念”一节,用以描述领域对象。例如,S3里定义了Bucket、Object和Permission等领域对象,Kinesis里定义了流(stream)和分片(shard)。先了解一个服务的领域对象,再查看API参考,然后浏览服务的API清单。你会发现,基于这些领域对象构建的API在理解和使用上都更加直观。

查看英文原文: There is No U in CRUD

转自 http://www.infoq.com/cn/articles/there-is-no-u-in-crud