DDD应用程序服务和REST API之间的概念不匹配


20

我正在尝试设计一个具有复杂业务域和支持REST API(严格来说不是REST,而是面向资源)的应用程序。我想出一种以面向资源的方式公开域模型的方法时遇到一些麻烦。

在DDD中,域模型的客户端需要通过过程“应用程序服务”层来访问由实体和域服务实现的任何业务功能。例如,有一个具有两种方法来更新User实体的应用程序服务:

userService.ChangeName(name);
userService.ChangeEmail(email);

此应用程序服务的API公开命令(动词,过程),而不是状态。

但是,如果我们还需要为同一应用程序提供RESTful API,则有一个用户资源模型,如下所示:

{
name:"name",
email:"email@mail.com"
}

面向资源的API公开状态,而不是命令。这引起了以下问题:

  • 根据REST API上的每个属性,每个针对REST API的更新操作都可以映射到一个或多个Application Service过程调用。

  • 对于REST API客户端,每个更新操作看起来都是原子操作,但是并不是那样实现的。每个应用程序服务调用被设计为一个单独的事务。在资源模型上更新一个字段可能会更改其他字段的验证规则。因此,我们需要一起验证所有资源模型字段,以确保所有潜在的应用程序服务调用在开始创建之前都是有效的。一次验证一组命令要比一次执行简单得多。我们如何在甚至不知道存在单个命令的客户端上执行此操作?

  • 以不同的顺序调用Application Service方法可能会产生不同的效果,而REST API看起来没有什么区别(在一个资源内)

我可以想出更多类似的问题,但是基本上它们都是由同一件事引起的。每次调用应用程序服务后,系统状态都会更改。什么是有效更改的规则,即实体可以执行下一个更改的一组操作。面向资源的API试图使它们看起来都像原子操作。但是跨越这个鸿沟的复杂性一定要到某个地方,而且看起来是巨大的。

另外,如果UI更加面向命令(通常是这种情况),那么我们将不得不在客户端的命令和资源之间进行映射,然后再在API端进行映射。

问题:

  1. 所有这些复杂性是否应该仅由(厚)REST到AppService映射层处理?
  2. 还是我对DDD / REST的理解缺少什么?
  3. REST是否对于在一定程度(相当低)的复杂度下公开域模型功能不可行?


将REST客户端视为系统的用户。他们完全不关心系统如何执行其执行的动作。您再也不会期望REST客户端知道域上所有不同的操作,而您不会期望用户知道。就像您说的那样,此逻辑必须走到某个地方,但在任何系统中都必须走到某个地方,如果您不使用REST,您只会将其上移到客户端中。不这样做恰恰是REST的重点,客户端应该只知道它想要更新状态,并且不知道该如何处理。
Cormac Mulhall 2015年

2
@astr简单的答案是资源不是您的模型,因此资源处理代码的设计不应影响模型的设计。资源是系统的一个面向外部的方面,因为模型是内部的。以与您想到UI相同的方式来考虑资源。用户可能会在UI上单击一个按钮,然后在模型中发生一百种不同的事情。类似于资源。客户端更新资源(单个PUT语句),该模型中可能发生一百万种不同的情况。将模型与资源紧密耦合是一种反模式。
Cormac Mulhall 2015年

1
关于如何将您的域中的操作作为REST状态更改的副作用进行讨论,这是一个很好的话题,使您的域和Web保持独立(多点前进到25分钟)yow.eventer.com/events/1004/talks/1047
Cormac Mulhall 2015年

1
我也不确定整个“作为机器人/状态机的用户”的事情。我认为我们应该努力使我们的用户界面更加自然……
guillaume31

Answers:


10

我遇到了同样的问题,并通过对REST资源进行不同的建模来“解决”,例如:

/users/1  (contains basic user attributes) 
/users/1/email 
/users/1/activation 
/users/1/address

因此,我基本上将较大的复杂资源拆分为几个较小的资源。这些属性中的每一个都包含原始资源的某些内聚属性组,这些属性应一起处理。

这些资源上的每个操作都是原子性的,即使可以使用几种服务方法来实现-至少在Spring / Java EE中,使用最初打算拥有自己的事务的几种方法(使用REQUIRED事务)创建更大的事务并不是问题。传播)。您通常仍然需要对此特殊资源进行额外的验证,但是由于属性(应该是)是内聚的,因此它仍然非常易于管理。

这对于HATEOAS方法也有好处,因为您的更细粒度的资源传达了有关可以使用它们的功能的更多信息(而不是在客户端和服务器上都具有此逻辑,因为它无法轻松地在资源中表示)。

当然,这不是完美的-如果不考虑这些资源(尤其是面向数据的UI)来建模UI,则可能会产生一些问题-例如,UI呈现给定资源(及其子资源)的所有属性的大形式,并允许您全部编辑并立即保存-即使客户端必须调用多个资源操作(它们本身是原子的,但整个序列不是原子的),这也会造成原子性的错觉。

而且,这种资源分配有时并不容易或不明显。我主要针对具有复杂行为/生命周期的资源来管理其复杂性。


这也是我一直在想的-创建更精细的资源表示形式,因为它们对于写操作更方便。当资源变得如此精细时,如何处理资源查询?还创建只读的非规范化表示形式吗?
astreltsov 2015年

1
不,我没有只读的非规范化表示形式。我使用jsonapi.org标准,它具有一种机制,可以在给定资源的响应中包含相关资源。基本上,我说的是“给我ID为1的用户,还包括其子资源电子邮件和激活”。这有助于摆脱对子资源的额外REST调用,并且如果您使用一些良好的JSON API客户端库,也不会影响客户端处理子资源的复杂性。
qbd

因此,服务器上的单个GET请求会转换为一个或多个实际查询(取决于所包含的子资源的数量),然后将这些查询合并为一个资源对象?
astreltsov 2015年

如果需要一层以上的嵌套怎么办?
astreltsov 2015年

是的,在关系数据库中,这可能会转换为多个查询。JSON API支持任意嵌套,其描述如下:jsonapi.org/format/#fetching-includes
qbd

0

这里的关键问题是,在进行REST调用时如何透明地调用业务逻辑?这是REST无法直接解决的问题。

我已经通过在诸如JPA之类的持久性提供程序上创建自己的数据管理层来解决了这一问题。使用带有自定义注释的元模型,当实体状态更改时,我们可以调用适当的业务逻辑。这样可以确保不管实体状态如何更改都将调用业务逻辑。它将您的架构保持干燥,并将您的业务逻辑放在一个地方。

使用上面的示例,当使用REST更改名称字段时,我们可以调用称为validateName的业务逻辑方法:

class User { 
      String name;
      String email;

      /**
       * This method will be transparently invoked when the value of name is changed
       * by REST.
       * The XorUpdate annotation becomes effective for PUT/POST actions
       */
      @XorPostChange
      public void validateName() {
        if(name == null) {
          throw new IllegalStateException("Name cannot be set as null");
        }
      }
    }

使用这样的工具,您需要做的就是适当地注释业务逻辑方法。


0

我想出一种以面向资源的方式公开域模型的方法时遇到一些麻烦。

您不应该以面向资源的方式公开域模型。您应该以面向资源的方式公开应用程序。

如果UI更加面向命令(通常是这种情况),那么我们将不得不在客户端的命令和资源之间进行映射,然后再在API端进行映射。

完全没有-将命令发送到与域模型接口的应用程序资源。

根据REST API上的每个属性,每个针对REST API的更新操作都可以映射到一个或多个Application Service过程调用。

是的,尽管有一些稍微不同的拼写方式可以使事情变得简单。针对REST api的每个更新操作都映射到一个将命令分配给一个或多个聚合的进程。

对于REST API客户端,每个更新操作看起来都是原子操作,但是并不是那样实现的。每个应用程序服务调用被设计为一个单独的事务。在资源模型上更新一个字段可能会更改其他字段的验证规则。因此,我们需要一起验证所有资源模型字段,以确保所有潜在的应用程序服务调用在开始创建之前都是有效的。一次验证一组命令要比一次执行简单得多。我们如何在甚至不知道存在单个命令的客户端上执行此操作?

您在这里追错了尾巴。

想象一下:将REST完全删除。想象一下,您正在为该应用程序编写桌面界面。让我们进一步想象一下,您确实有很好的设计要求,并且正在实现基于任务的UI。因此,用户可以获得一个极简的界面,该界面针对他们正在执行的任务进行了完美调整。用户指定一些输入,然后点击“动词!” 按钮。

现在会发生什么?从用户的角度来看,这是要完成的单个原子任务。从domainModel的角度来看,它是由聚合运行的许多命令,其中每个命令在单独的事务中运行。这些完全不兼容!我们需要中间的东西来弥合差距!

就是“应用程序”。

在快乐的道路上,应用程序接收一些DTO,然后解析该对象以获取其可以理解的消息,并使用消息中的数据为一个或多个聚合创建格式正确的命令。应用程序将确保分配给聚合的每个命令格式正确(这是工作中的反腐败层),并且它将加载聚合,并在事务成功完成时保存聚合。给定其当前状态,聚合将自行决定命令是否有效。

可能的结果-所有命令都成功运行-反腐败层拒绝了该消息-一些命令成功运行,但是其中一个聚集抱怨了,您就有了缓解的可能。

现在,假设您已经构建了该应用程序;您如何以RESTful方式与其交互?

  1. 客户端以其当前状态的超媒体描述(即,基于任务的UI)开始,包括超媒体控件。
  2. 客户端将任务的表示形式(即DTO)分派给资源。
  3. 资源解析传入的HTTP请求,获取表示,然后将其交给应用程序。
  4. 应用程序运行任务;从资源的角度来看,这是一个黑匣子,具有以下结果之一
    • 应用程序成功更新了所有聚合:资源向客户端报告成功,将其定向到新的应用程序状态
    • 反腐败层拒绝该消息:资源向客户端报告4xx错误(可能是错误请求),可能会传递所遇到问题的描述。
    • 应用程序将更新一些汇总信息:资源向客户端报告命令已被接受,并将客户端定向到将提供命令进度表示的资源。

当应用程序将处理消息推迟到响应客户端之后,通常接受的结果是接受的-通常在接受异步命令时使用。但这对于这种情况也很有效,在这种情况下,原本应该是原子的操作需要缓解。

在这个惯用语中,资源表示任务本身-通过将适当的表示形式发布到任务资源来启动任务的新实例,并且该资源与应用程序接口并将您定向到下一个应用程序状态。

,几乎每当您协调多个命令时,您都希望根据流程(aka业务流程,aka saga)进行思考。

读取模型中存在类似的概念不匹配。再次考虑基于任务的界面;如果任务需要修改多个聚合,则用于准备任务的UI可能包括来自多个聚合的数据。如果您的资源计划是1:1的聚合,那就很难安排了;而是提供一个从几个聚合返回数据表示形式的资源,以及一个将“开始任务”关系映射到任务端点的超媒体控件,如上所述。

另请参见:Jim Webber撰写的REST in Practice


如果我们正在设计API以根据用例与我们的领域进行交互。为什么不以根本不需要Sagas的方式设计事物?也许我缺少了一些东西,但是通过阅读您的回答,我真的相信REST与DDD并不是很好的匹配,最好使用远程过程(RPC)。DDD以行为为中心,而REST以http-verb为中心。为什么不从图片中删除REST并公开API中的行为(命令)?毕竟,它们可能被设计为满足用例场景,并且概率是事务性的。如果我们拥有UI,REST有什么优势?
iberodev
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.