Rest API和DDD

时间:2016-02-29 13:05:02

标签: api rest domain-driven-design

在我使用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的文章。到处都只有一个理论,如何与我的具体问题在一起?

4 个答案:

答案 0 :(得分:26)

  

我在网上看过很多关于REST的文章。

根据我在这里看到的内容,您至少需要观看Jim Webber关于REST和DDD的谈话

  

但是如何处理其他用例?

暂时忽略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只是传递小文件来推进状态机。

答案 1 :(得分:15)

独立于域图层设计您的rest api。

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

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

据我所知,你有一个Deal资源。如你所说,创建/删除很容易:

  • POST / rest / {version} / deals
  • DELETE / rest / {version} / deals / {id}。

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

PUT / rest / {version} / deals / {id}

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

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

POST / rest / {version} / deals / {id} / hold

{
    reason: "something"
}

另一个解决方案:忘记REST理论。如果你认为你的api会更清晰,更高效,更简单,在网址中使用动词,那么一定要做到这一点。你可能找到一种方法来避免它,但如果你不能,不要做一些丑陋的事情只是因为它是常态。

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

我不能为你设计你的api,你是唯一知道你的用例的人,但我会再说一遍我的建议:

  • 自行设计其余的api,然后使用应用程序层以正确的顺序调用适当的域对象。这正是应用程序层的用途。
  • 不要盲目遵循规范和理论。是的,你应该尽可能地遵循良好的做法和规范,但如果你不能把它们抛在脑后(经过仔细考虑后)

答案 2 :(得分:2)

文章Exposing CQRS Through a RESTful API是解决您的问题的详细方法。您可以检查prototype API。一些评论:

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

答案 3 :(得分:0)

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

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

例如: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边界,它查找命令/查询,并执行适当的应用程序服务。