为什么域模型不应用作 REST API 中的资源?

Why the domain model should not be used as resources in REST API?

我看到一个声明,根据 DDD 设计的域模型不应用作 REST API (source) 中的资源。

很明显,REST API 是应用程序的契约,而域模型是实现的一部分,因此最好将这两件事分开,以便更改域模型不会自动暗示 REST API.

中的更改

但是,我认为在小型项目的情况下(其中 REST API 只有一个消费者 - javascript 前端,由一个团队开发)拥有单独模型的好处并不能证明分离模型的成本(不同 类 - 域模型和资源表示以及模型之间的映射代码)。显然,域层不能引用 REST 特定基础架构代码(以保持关注点分离)。

域和 REST 模型应该分开吗?

使用 DDD 时,REST API 应始终与领域模型分开。

这样做的主要原因是简化 - 您不想通过 API 将域模型的复杂性泄露给客户端。否则,客户需要了解您域的细微差别和复杂性,这很可能使 API 难以使用。

并且使用 DDD 的主要驱动因素是一个复杂的问题领域,所以这总是一个问题。

However, I think in case of small projects (…) the benefits of having separate models does not justify the cost of separating the models (…).

我同意有些项目将域模型和 REST API 分开是过度设计的。但是,这些案例不是 DDD 的候选对象,因为您从 DDD 中获益不足以证明其成本合理。

我认为要考虑的另一件事是谁使用您的 REST API。如果您正在为应用程序开发前端,那么您可以说一切仍在 1 个有界上下文中发生。只有一部分生活在 client/javascript。在那种情况下,我认为在休息时公开您的模型确实有意义api。

在这种情况下,REST api 可能只是与您的域服务进行通信的一种方式。

Why the domain model should not be used as resources in REST API?

因为网络是一个与您的核心域层完全不同的世界。实体中的方法特别难以翻译,因为 HTTP 只有少数动词。如果您想通过 REST 公开您的应用程序,您必须将您的域进程硬塞进 HTTP,这通常意味着做出妥协并设计与您的域实体不同的资源。

当然,如果您正在执行 HATEOAS,您应该在 HTTP 客户端和服务器之间交换的消息以及域应用程序协议中找到来自通用语言的术语,但是网络必然会扭曲您的域表示。

REST 的重点不是重新创建域及其流程的高保真模型,而是以符合 HTTP 的方式交付它们,同时尽可能少地在转换中丢失。但它仍然是一个翻译。

您可以根据域模型将业务逻辑挂钩到 REST 资源中。例如,每当有人设置 is_published = 1 时,您都可以通过挂接到事件或修改器来通知管理员、进行额外验证等。有时事情可能太复杂和奇怪而无法这样做,因此您可以将某些属性标记为不可修改,然后创建自定义操作来修改您公开的它们,如果这有意义的话。我认为如果你设计得当,你甚至不需要这些 "custom actions"。 Facebook 不使用任何图表 API 我不认为。我正在考虑开发一个仅基于暴露模型层的框架,我仍然认为这是个好主意。

我认为 REST APIs 的主要好处是为(通常是服务器端而不是 SPA)第 3 方 REST 客户端提供服务。如果您使用 HATEOAS 和其他自描述消息解决方案(如 RDF),那么 REST 客户端将由于 REST API 的变化而更加困难。对于小型项目 - "where the REST API has just one consumer - the javascript frontend, developed by one team" - 我认为拥有适当的 REST API 是不值得的。大多数人使用它的简化版本,我称之为 CRUD API,这些可能对这些项目有好处。

CRUD 资源和贫血领域模型的领域对象之间可以存在一对一的映射。如果我们谈论的不仅仅是 CRUD 方法的真实对象(而不是数据结构),那么你必须在 resource.verb 和 object.method 之间进行转换,例如:

POST /dogs/{id}/barking
 -> domain.dog.bark()

如果我们谈论的是涉及多个域对象和工作单元(事务)的更复杂的事情,那么您需要为应用程序服务添加另一层,否则您会将包括事务处理在内的整个复杂操作移至客户端。在这些情况下,您可以在 resource.verb 和 applicationService.operation 之间进行翻译,例如:

POST /dogs/{id1,id2,..}/barking
 -> dogService.multiDogBark(...)
 -> UnitOfWork{domain.dogs[ids[i]].bark()}

我认为大多数开发人员将这种 CRUD 服务 + 贫血域模型方法与 REST 服务 + 域模型方法混淆,这就是为什么会问这个问题,这就是为什么有许多 "REST" 框架添加 1:1 域对象 - CRUD 资源映射,甚至可能是 ORM 实体 - CRUD 资源映射。我发现这种趋势非常具有破坏性,我认为主要原因是开发人员只是从简短的文章或问答网站上肤浅地学习某些技术,而不是阅读书籍和论文,在那里他们可以深入了解实际主题。我认为这是 Y+ 一代的问题,因为数字技术的使用,我们正在失去阅读长文本的能力。我们习惯于即时奖励而不是长文本给出的延迟奖励...