BT

如何利用碎片时间提升技术认知与能力? 点击获取答案

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

| 作者 James Hood 关注 0 他的粉丝 ,译者 薛命灯 关注 19 他的粉丝 发布于 2017年9月19日. 估计阅读时间: 7 分钟 | QCon北京2018全面起航:开启与Netflix、微软、ThoughtWorks等公司的技术创新之路!

亲爱的读者:我们最近添加了一些个人消息定制功能,您只需选择感兴趣的技术主题,即可获取重要资讯的邮件和网页通知

来自亚马逊的高级工程师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


感谢雨多田光对本文的审校。

给InfoQ中文站投稿或者参与内容翻译工作,请邮件至editors@cn.infoq.com。也欢迎大家通过新浪微博(@InfoQ@丁晓昀),微信(微信号:InfoQChina)关注我们。

评价本文

专业度
风格

您好,朋友!

您需要 注册一个InfoQ账号 或者 才能进行评论。在您完成注册后还需要进行一些设置。

获得来自InfoQ的更多体验。

告诉我们您的想法

允许的HTML标签: a,b,br,blockquote,i,li,pre,u,ul,p

当有人回复此评论时请E-mail通知我

还是要区分API使用场景 by Ethan Liu

debit和credit如果分开提供意味着客户端转账时需要自行控制事务完整性,不如直接提供类似cash transfer这样的service API。对大部分私有API来说,提供如此细粒度的API并没有必要。

一句话概括 by shen zhenghua

观点就是:通过URL就可以知道要做什么操作,而不是用一堆逻辑去解析参数。
不知理解是否正确?

Re: 一句话概括 by 高 岳农

以业务模型为中心构建 Rest 接口,而不是以数据库表(数据模型)为中心来提供 API。
目的是使接口的作用能做到自解释,业务模型的抽象也更完整。

Re: 还是要区分API使用场景 by ao yunfeng

我认为作者说的没错,DDD中账户为一个领域对象,其下都是属于账户的一些操作。你举的那个例子,涉及多个账户操作,本身根据ddd设计,应该就是一个领域服务对象,而不应放到账户对象里。

Re: 还是要区分API使用场景 by Ethan Liu

所以我说从设计REST API的角度来说,这是要把领域服务放到客户端实现么?不敢苟同

允许的HTML标签: a,b,br,blockquote,i,li,pre,u,ul,p

当有人回复此评论时请E-mail通知我

允许的HTML标签: a,b,br,blockquote,i,li,pre,u,ul,p

当有人回复此评论时请E-mail通知我

5 讨论

登陆InfoQ,与你最关心的话题互动。


找回密码....

Follow

关注你最喜爱的话题和作者

快速浏览网站内你所感兴趣话题的精选内容。

Like

内容自由定制

选择想要阅读的主题和喜爱的作者定制自己的新闻源。

Notifications

获取更新

设置通知机制以获取内容更新对您而言是否重要

BT