REST API 和 DDD

Posted

技术标签:

【中文标题】REST API 和 DDD【英文标题】:Rest API and DDD 【发布时间】:2016-06-12 12:56:02 【问题描述】:

在我的项目中使用 DDD 方法。

该项目有聚合(实体)交易。这个聚合有很多用例。

对于这个聚合,我需要创建一个 rest api。

用标准:创建和删除没问题。

1) CreateDealUseCase(名称、价格和许多其他参数);

POST /rest/version/deals/
 
   'name': 'deal123',
   'price': 1234;
   'etc': 'etc'

2) DeleteDealUseCase(id)

DELETE /rest/version/deals/id

但是如何处理其余的用例呢?

HoldDealUseCase(id, reason); UnholdDealUseCase(id); CompleteDealUseCase(id 和许多其他参数); CancelDealUseCase(id, amercement, reason); ChangePriceUseCase(id, newPrice, reason); ChangeCompletionDateUseCase(id, newDate, amercement, whyChanged); 等(共20个用例)...

有什么解决办法?

1) 使用动词

PUT /rest/version/deals/id/hold
 
   'reason': 'test'

但是! url中不能使用动词(在REST理论中)。

2) 使用完成状态(将在用例之后):

PUT /rest/version/deals/id/holded
 
   'reason': 'test'

就我个人而言,它看起来很丑。也许我错了?

3) 对所有操作使用 1 个 PUT 请求:

PUT /rest/version/deals/id
 
   'action': 'HoldDeal',
   'params': 'reason': 'test'


PUT /rest/version/deals/id
 
   'action': 'UnholdDeal',
   'params': 

很难在后端处理。 此外,很难记录。由于 1 个操作有许多不同的请求变体,这些变体已经依赖于特定的响应。

所有解决方案都有明显的缺点。

我在互联网上阅读了很多关于 REST 的文章。到处都是理论,我的具体问题怎么来这里?

【问题讨论】:

我不想将以下内容作为答案,因此如果这是一个糟糕的想法,也许其他人可以发表意见。怎么样:/rest/version/dealsheld//rest/version/dealscompleted/id 等。因为在任何情况下都需要知道您正在处理的状态。这样的方案有意义吗? 【参考方案1】:

独立于领域层设计你的 rest api。

领域驱动设计的一个关键概念是不同软件层之间的低耦合。所以,当你设计你的 rest api 时,你会考虑你可以拥有的最好的 rest api。然后,应用层的角色就是调用领域对象来执行所需的用例。

我无法为你设计你的 rest api,因为我不知道你想做什么,但这里有一些想法。

据我了解,您拥有交易资源。正如你所说,创建/删除很容易:

POST /rest/version/deals 删除 /rest/version/deals/id。

然后,您想“持有”一笔交易。我不知道这意味着什么,您必须考虑它在资源“交易”中的变化。它会改变一个属性吗?如果是,那么您只是在修改 Deal 资源。

PUT /rest/version/deals/id


    ...
    held: true,
    holdReason: "something",
    ...

它增加了什么吗?您可以对交易进行多次保留吗?在我看来,“持有”是一个名词。如果它很难看,就找一个更好的名词。

POST /rest/version/deals/id/holds


    reason: "something"

另一个解决方案:忘记 REST 理论。如果您认为在 url 中使用动词会使您的 api 更清晰、更高效、更简单,那么请务必这样做。你或许可以找到一种方法来避免它,但如果你做不到,就不要因为这是常态而做出丑陋的事情。

看看twitter's api:很多开发人员说twitter 有一个设计良好的API。 Tadaa,它使用动词!谁在乎,只要它既酷又好用?

我无法为你设计你的 api,你是唯一知道你的用例的人,但我再说一遍我的两个建议:

自己设计rest api,然后使用应用层按正确的顺序调用相应的领域对象。这正是应用层的用途。 不要盲目地遵循规范和理论。是的,您应该尽可能地遵循良好的做法和规范,但如果不能,则将它们抛在脑后(当然要经过仔细考虑)

【讨论】:

领域驱动设计是关于领域的。 API 客户端的设计也应该考虑到领域。否则,您将失去 DDD 的大部分优势。 是的,但这并不意味着您应该将域的所有复杂性暴露给 API 的使用者。例如,API 可以公开域层功能的子集。 你不应该暴露命令处理程序或其他内部的逻辑。但它涉及聚合命令。不是所有的复杂性,它是域的公共形式。【参考方案2】:

我在互联网上阅读了很多关于 REST 的文章。

根据我在这里看到的,您确实需要至少观看一次 Jim Webber 关于 REST 和 DDD 的演讲

Rest in Practice Domain Driven Design for RESTful Systems

但是如何处理其余的用例呢?

暂时忽略 API - 您将如何处理 html 表单?

您可能有一个网页,其中显示了Deal 的表示,上面有一堆链接。一个链接会将您带到HoldDeal 表单,另一个链接会将您带到ChangePrice 表单,依此类推。这些表单中的每一个都需要填写零个或多个字段,并且每个表单都会发布到某个资源以更新域模型。

他们都会发布到同一个资源吗?也许,也许不是。它们都将具有相同的媒体类型,因此如果它们都发布到同一个 Web 端点,您将不得不解码另一端的内容。

鉴于这种方法,您如何实施您的系统?好吧,根据您的示例,媒体类型希望为 json,但其余部分确实没有任何问题。

1) 使用动词:

没关系。

但是! url中不能使用动词(在REST理论中)。

嗯……不。 REST 不关心资源标识符的拼写。有一堆 URI 最佳实践声称动词是不好的——这是真的——但这不是 REST 遵循的。

但是如果人们这么挑剔,你可以为命令命名端点而不是动词。 (即:“hold”不是动词,它是一个用例)。

对所有操作使用 1 个 PUT 请求:

老实说,那个也不错。虽然您不想共享 URI(因为 PUT 方法的指定方式),但使用客户端可以指定唯一标识符的模板。

重点是:您正在基于 HTTP 和 HTTP 方法构建 API。 HTTP 专为文档传输而设计。客户端给你一个文档,描述你的域模型中请求的更改,你将更改应用到域(或不应用),并返回另一个描述新状态的文档。

暂时借用 CQRS 词汇,您正在发布命令以更新您的域模型。

PUT /commands/commandId
 
   'deal' : dealId
   'action': 'HoldDeal',
   'params': 'reason': 'test'

理由 - 您正在将特定命令(具有特定 Id 的命令)放入命令队列,这是一个集合。

PUT /rest/version/deals/dealId/commands/commandId
 
   'action': 'HoldDeal',
   'params': 'reason': 'test'

是的,那也很好。

再看看 RESTBucks。这是一个咖啡店协议,但所有的 api 只是传递小文件来推进状态机。

【讨论】:

我好像是你发明了基于 REST 的远程过程调用。 但是,如果您不想构建 20 个遵循域模型行为的端点怎么办? 20 个端点极难维护。如果您在应用程序层和域层之间有一个端点和一个附加层来比较和处理发布的数据以触发正确的域行为,该怎么办? 无论哪种方式都存在复杂性。您可以将其放在其余客户端或服务器中。在服务器中,您可以减少域规则所在的位置。 它不再是您所说的 REST。同意@xfg 我几乎不认为这是一个全有或全无的情况。我有构建 RESTful API 的经验,几年后它显示出类似于 OP 的迹象,我们在公司将其称为 Anemic REST。在我们的案例中,这是一个大型依赖树的问题,使得操作成本非常高。因此,我想说,如果你能识别出可能不符合“纯”REST 的用例,那就这样吧。以对领域和可维护性有意义的最佳方式解决它。但无论如何不要完全放弃 REST,在有意义的时候使用它。【参考方案3】:

我将用例 (UC) 分为 2 组:命令和查询 (CQRS),并且我有 2 个 REST 控制器(一个用于命令,另一个用于查询)。由于 POST/GET/PUT/DELETE 的结果,REST 资源不必是模型对象即可对其执行 CRUD 操作。资源可以是您想要的任何对象。实际上,在 DDD 中,您不应该将域模型暴露给控制器。

(1) RestApiCommandController: 每个命令用例一个方法。 URI 中的 REST 资源是命令类名称。该方法始终是 POST,因为您创建命令,然后通过命令总线(在我的情况下为中介)执行它。请求正文是映射命令属性(UC 的 args)的 JSON 对象。

例如:http://localhost:8181/command/asignTaskCommand/

@RestController
@RequestMapping("/command")
public class RestApiCommandController 

private final Mediator mediator;    

@Autowired
public RestApiCommandController (Mediator mediator) 
    this.mediator = mediator;
    

@RequestMapping(value = "/asignTaskCommand/", method = RequestMethod.POST)
public ResponseEntity<?> asignTask ( @RequestBody AsignTaskCommand asignTaskCommand )      
    this.mediator.execute ( asigTaskCommand );
    return new ResponseEntity ( HttpStatus.OK );


(2) RestApiQueryController: 每个查询用例一个方法。这里 URI 中的 REST 资源是查询返回的 DTO 对象(作为集合的元素,或仅作为一个元素)。方法始终是 GET,查询 UC 的参数是 URI 中的参数。

例如:http://localhost:8181/query/asignedTask/1

@RestController
@RequestMapping("/query")
public class RestApiQueryController 

private final Mediator mediator;    

@Autowired
public RestApiQueryController (Mediator mediator) 
    this.mediator = mediator;
    

@RequestMapping(value = "/asignedTask/employeeId", method = RequestMethod.GET)
public ResponseEntity<List<AsignedTask>> asignedTasksToEmployee ( @PathVariable("employeeId") String employeeId ) 

    AsignedTasksQuery asignedTasksQuery = new AsignedTasksQuery ( employeeId);
    List<AsignedTask> result = mediator.executeQuery ( asignedTasksQuery );
    if ( result==null || result.isEmpty() ) 
        return new ResponseEntity ( HttpStatus.NOT_FOUND );
    
    return new ResponseEntity<List<AsignedTask>>(result, HttpStatus.OK);
 

注意: Mediator 属于 DDD 应用层。它是 UC 边界,它查找命令/查询,并执行相应的应用程序服务。

【讨论】:

【参考方案4】:

文章Exposing CQRS Through a RESTful API 是解决您的问题的详细方法。您可以查看prototype API。几个cmets:

这是一种复杂的方法,因此您可能不需要实现文章中的所有内容:通过 HTTP 的 ETag 和 If-Match 的事件溯源并发是一种“高级”功能 这是一种自以为是的方法:DDD 命令类型是通过媒体类型标头发送的,而不是通过正文发送的。就我个人而言,我觉得这很有趣……但不确定是否会以这种方式实现

【讨论】:

以上是关于REST API 和 DDD的主要内容,如果未能解决你的问题,请参考以下文章

如果 REST 控制器类和接口具有使用 @HystrixCommand 注释的 API,则不会加载所有 REST API

SOAP和REST API测试技巧

什么是REST API

什么是REST API?

JSON:带有 django-rest-framework-json-api 和 JWT 的 API

软件定义网络基础---REST API概述