写点什么

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

  • 2017-09-18
  • 本文字数:2187 字

    阅读完需:约 7 分钟

来自亚马逊的高级工程师 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/ /close ——注销一个已有的账户。
  3. PUT /account/ /debit ——从账户里扣掉一些钱。
  4. PUT /account/ /credit ——往账户里存入一些钱。
  5. GET /account/ ——通过账户 ID 加载相应的账户信息。
  6. GET /account/ /transactions ——列出账户的交易历史。
  7. GET /accounts/query/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 )关注我们。

2017-09-18 16:596254
用户头像

发布了 322 篇内容, 共 146.3 次阅读, 收获喜欢 148 次。

关注

评论

发布
暂无评论
发现更多内容

首个云上 AI 原生全栈可观测平台来了!

阿里巴巴云原生

阿里云 云原生

基于低代码的企业级数字化转型实践:技术生态构建与全场景应用架构演进

不在线第一只蜗牛

低代码 数字化

从战略规划到持续创新:如何跟上不断变化的商业环境

智达方通

数据分析 财务分析 全面预算管理 财务管理 财务规划

AI 重塑宗教体验,语音 Agent 能否成为突破点?

声网

重磅发布!AI 驱动的 Java 开发框架:Spring AI Alibaba

阿里巴巴云原生

阿里云 微服务 云原生

飞算 JavaAI:开发界的 “AI 教练”,助你飞速成长!

飞算JavaAI开发助手

飞算 JavaAI:开发界的 “时间管理大师”,让你效率倍增!

飞算JavaAI开发助手

云消息队列 ApsaraMQ Serverless 演进:高弹性低成本、更稳定更安全、智能化免运维

阿里巴巴云原生

阿里云 云原生 消息队列

飞算 JavaAI,编程界的 “哪吒降世”,十倍提效不是梦!

飞算JavaAI开发助手

何必把DeepSeek推上神坛?

脑极体

AI

做新加坡TikTok直播要不要专线网络?

Ogcloud

直播专线 tiktok直播 tiktok直播专线 tk直播专线

1个小技巧彻底解决DeepSeek服务繁忙!

王磊

世界一流财务管理体系建设“4-3-9”模型!

用友智能财务

Byteman 使用指南(六)

FunTester

云厂商的DeepSeek大捷

脑极体

AI

“轻松上手!5分钟学会用京东云打造你自己的专属DeepSeek”

京东科技开发者

TikTok海外直播网络专线的优势

Ogcloud

TikTok 直播专线 tiktok直播 tiktok直播专线 tiktok直播网络

做好供应链计划 开年业绩翻倍儿

第七在线

飞算 JavaAI:需求、接口、代码,一键全搞定!

飞算JavaAI开发助手

Star 4w+,Apache Dubbo 3.3 全新发布,Triple X 领衔,开启微服务通信新时代

阿里巴巴云原生

阿里云 微服务 云原生 开源w

Byteman 使用指南(五)

FunTester

VMware NSX Advanced Load Balancer (NSX ALB) 30.1.2 - 多云负载均衡平台

sysin

小红书开源 FireRedASR 语音识别模型,3.05% 字错误率;Meta AI 脑电波打字模型:32% 字错误率

声网

用友BIP:智能体技术引领企业服务变革

用友BIP

人工智能 智能体 用友BIP 用友软件 用友网络

一毛钱畅享4070云电脑!ToDesk让旧电脑焕发新生

小喵子

云电脑 云游戏 ToDesk ToDesk云电脑 云电竞

为什么说要用DDD替代CRUD来设计API_语言 & 开发_James Hood_InfoQ精选文章