休息 API 和 DDD
Rest API and DDD
在我的项目中使用 DDD 方法。
该项目有聚合(实体)交易。这个聚合有很多用例。
对于这个聚合,我需要创建一个休息 api。
使用标准:创建和删除没问题。
1) CreateDealUseCase(名称、价格和许多其他参数);
POST /rest/{version}/deals/
{
'name': 'deal123',
'price': 1234;
'etc': 'etc'
}
2) DeleteDealUseCase(id)
DELETE /rest/{version}/deals/{id}
但是如何处理其余用例?
- HoldDealUseCase(id, 原因);
- 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的文章。到处都是理论,怎么会出现我的具体问题?
独立于域层设计您的其余部分api。
领域驱动设计的一个关键概念是不同软件层之间的低耦合。因此,当您设计休息 api 时,您会考虑您可以拥有的最佳休息 api。然后,应用层的作用就是调用领域对象来执行所需的用例。
我无法为你设计休息api,因为我不知道你想做什么,但这里有一些想法。
据我了解,您有一个交易资源。正如您所说,creation/deletion 很简单:
- POST/rest/{version}/deals
- 删除/rest/{version}/deals/{id}。
那么,你要"hold"一笔交易。我不知道那是什么意思,你得想想它在资源"Deal"中改变了什么。它会改变属性吗?如果是,那么您只是在修改 Deal 资源。
PUT /rest/{version}/deals/{id}
{
...
held: true,
holdReason: "something",
...
}
它添加了什么吗?您可以多次持有 Deal 吗?在我看来 "hold" 是一个名词。丑的话找个更好的名词吧
POST/rest/{version}/deals/{id}/holds
{
reason: "something"
}
另一个解决方案:忘记 REST 理论。如果你认为你的 api 在 url 中使用动词会更清晰、更有效、更简单,那么一定要这样做。你或许可以找到一种方法来避免它,但如果你不能,不要仅仅因为这是常态就做一些丑陋的事情。
看看 twitter's api:很多开发者都说 twitter 的设计很好 API。 Tadaa,它使用动词!谁在乎,只要它很酷且易于使用即可?
我无法为你设计你的api,你是唯一知道你的用例的人,但我会再说一遍我的两个建议:
- 剩下的api自己设计,然后使用应用层按正确的顺序调用合适的领域对象。这正是应用层的用途。
- 不要盲目遵循规范和理论。是的,您应该尽可能地遵循良好的做法和规范,但如果您做不到,那就放弃它们(当然是在仔细考虑之后)
I have read many articles about the REST on the internet.
根据我在这里看到的内容,您真的需要至少看一场 Jim Webber 关于 REST 和 DDD 的演讲
But what to do with the rest of the use cases?
暂时忽略 API - 你会如何使用 HTML 表格?
您可能有一个网页,其中显示了 Deal
,上面有一堆 link。一个 link 会把你带到 HoldDeal
表格,另一个 link 会把你带到 ChangePrice
表格,依此类推。这些表单中的每一个都有零个或多个字段要填写,并且每个表单都会 post 一些资源以更新域模型。
他们会 post 到同一个资源吗?也许,也许不是。它们都具有相同的媒体类型,因此如果它们都 post 连接到同一个 Web 端点,您将必须解码另一端的内容。
鉴于这种方法,您如何实施您的系统?好吧,根据您的示例,媒体类型想要 json,但其余部分确实没有任何问题。
1) Use verbs:
没关系。
But! Verbs can not be used in the url(in REST theory).
嗯...不。 REST 不关心资源标识符的拼写。有很多 URI 最佳实践声称动词不好——这是真的——但这不是 REST 的结果。
但是如果人们太挑剔了,您可以为命令而不是动词命名端点。 (即:"hold" 不是动词,而是用例)。
Use 1 PUT request for all operations:
老实说,那个也不错。尽管您不想共享 URI(因为指定 PUT 方法的方式),但使用客户端可以指定唯一标识符的模板。
重点是:您正在 HTTP 和 HTTP 方法之上构建 API。 HTTP 专为文档传输 而设计。客户端给你一个文档,描述你的域模型中的请求更改,然后你将更改应用到域(或不应用),return 另一个描述新状态的文档。
暂时借用 CQRS 词汇表,您正在 posting 命令来更新您的领域模型。
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 只是传递小文档以推进状态机。
我将用例 (UC) 分为两组:命令和查询 (CQRS),并且我有 2 个 REST 控制器(一个用于命令,另一个用于查询)。由于 POST/GET/PUT/DELETE,REST 资源不必是模型对象就可以对其执行 CRUD 操作。资源可以是您想要的任何对象。事实上,在 DDD 中,您不应该将域模型暴露给控制器。
(1) RestApiCommandController: 每个命令用例一种方法。 URI 中的 REST 资源是命令 class 名称。该方法始终是 POST,因为您创建命令,然后通过命令总线(在我的例子中是中介)执行它。请求主体是映射命令属性(UC 的参数)的 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资源是查询returns的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 边界,它寻找 command/query,并执行适当的应用程序服务。
文章Exposing CQRS Through a RESTful API is a detailed approach addressing your problem. You can check the the prototype API。几点评论:
- 这是一种复杂的方法,因此您可能不需要实现文章中的所有内容:通过 HTTP 的 ETag 和 If-Match 的事件溯源并发就是这样一个 "advanced" 功能
- 这是一种固执己见的方法:DDD 命令类型是通过媒体类型 header 发送的,而不是通过 body 发送的。就我个人而言,我觉得它很有趣......但不确定如何实现这种方式
在我的项目中使用 DDD 方法。
该项目有聚合(实体)交易。这个聚合有很多用例。
对于这个聚合,我需要创建一个休息 api。
使用标准:创建和删除没问题。
1) CreateDealUseCase(名称、价格和许多其他参数);
POST /rest/{version}/deals/
{
'name': 'deal123',
'price': 1234;
'etc': 'etc'
}
2) DeleteDealUseCase(id)
DELETE /rest/{version}/deals/{id}
但是如何处理其余用例?
- HoldDealUseCase(id, 原因);
- 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的文章。到处都是理论,怎么会出现我的具体问题?
独立于域层设计您的其余部分api。
领域驱动设计的一个关键概念是不同软件层之间的低耦合。因此,当您设计休息 api 时,您会考虑您可以拥有的最佳休息 api。然后,应用层的作用就是调用领域对象来执行所需的用例。
我无法为你设计休息api,因为我不知道你想做什么,但这里有一些想法。
据我了解,您有一个交易资源。正如您所说,creation/deletion 很简单:
- POST/rest/{version}/deals
- 删除/rest/{version}/deals/{id}。
那么,你要"hold"一笔交易。我不知道那是什么意思,你得想想它在资源"Deal"中改变了什么。它会改变属性吗?如果是,那么您只是在修改 Deal 资源。
PUT /rest/{version}/deals/{id}
{
...
held: true,
holdReason: "something",
...
}
它添加了什么吗?您可以多次持有 Deal 吗?在我看来 "hold" 是一个名词。丑的话找个更好的名词吧
POST/rest/{version}/deals/{id}/holds
{
reason: "something"
}
另一个解决方案:忘记 REST 理论。如果你认为你的 api 在 url 中使用动词会更清晰、更有效、更简单,那么一定要这样做。你或许可以找到一种方法来避免它,但如果你不能,不要仅仅因为这是常态就做一些丑陋的事情。
看看 twitter's api:很多开发者都说 twitter 的设计很好 API。 Tadaa,它使用动词!谁在乎,只要它很酷且易于使用即可?
我无法为你设计你的api,你是唯一知道你的用例的人,但我会再说一遍我的两个建议:
- 剩下的api自己设计,然后使用应用层按正确的顺序调用合适的领域对象。这正是应用层的用途。
- 不要盲目遵循规范和理论。是的,您应该尽可能地遵循良好的做法和规范,但如果您做不到,那就放弃它们(当然是在仔细考虑之后)
I have read many articles about the REST on the internet.
根据我在这里看到的内容,您真的需要至少看一场 Jim Webber 关于 REST 和 DDD 的演讲
But what to do with the rest of the use cases?
暂时忽略 API - 你会如何使用 HTML 表格?
您可能有一个网页,其中显示了 Deal
,上面有一堆 link。一个 link 会把你带到 HoldDeal
表格,另一个 link 会把你带到 ChangePrice
表格,依此类推。这些表单中的每一个都有零个或多个字段要填写,并且每个表单都会 post 一些资源以更新域模型。
他们会 post 到同一个资源吗?也许,也许不是。它们都具有相同的媒体类型,因此如果它们都 post 连接到同一个 Web 端点,您将必须解码另一端的内容。
鉴于这种方法,您如何实施您的系统?好吧,根据您的示例,媒体类型想要 json,但其余部分确实没有任何问题。
1) Use verbs:
没关系。
But! Verbs can not be used in the url(in REST theory).
嗯...不。 REST 不关心资源标识符的拼写。有很多 URI 最佳实践声称动词不好——这是真的——但这不是 REST 的结果。
但是如果人们太挑剔了,您可以为命令而不是动词命名端点。 (即:"hold" 不是动词,而是用例)。
Use 1 PUT request for all operations:
老实说,那个也不错。尽管您不想共享 URI(因为指定 PUT 方法的方式),但使用客户端可以指定唯一标识符的模板。
重点是:您正在 HTTP 和 HTTP 方法之上构建 API。 HTTP 专为文档传输 而设计。客户端给你一个文档,描述你的域模型中的请求更改,然后你将更改应用到域(或不应用),return 另一个描述新状态的文档。
暂时借用 CQRS 词汇表,您正在 posting 命令来更新您的领域模型。
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 只是传递小文档以推进状态机。
我将用例 (UC) 分为两组:命令和查询 (CQRS),并且我有 2 个 REST 控制器(一个用于命令,另一个用于查询)。由于 POST/GET/PUT/DELETE,REST 资源不必是模型对象就可以对其执行 CRUD 操作。资源可以是您想要的任何对象。事实上,在 DDD 中,您不应该将域模型暴露给控制器。
(1) RestApiCommandController: 每个命令用例一种方法。 URI 中的 REST 资源是命令 class 名称。该方法始终是 POST,因为您创建命令,然后通过命令总线(在我的例子中是中介)执行它。请求主体是映射命令属性(UC 的参数)的 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资源是查询returns的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 边界,它寻找 command/query,并执行适当的应用程序服务。
文章Exposing CQRS Through a RESTful API is a detailed approach addressing your problem. You can check the the prototype API。几点评论:
- 这是一种复杂的方法,因此您可能不需要实现文章中的所有内容:通过 HTTP 的 ETag 和 If-Match 的事件溯源并发就是这样一个 "advanced" 功能
- 这是一种固执己见的方法:DDD 命令类型是通过媒体类型 header 发送的,而不是通过 body 发送的。就我个人而言,我觉得它很有趣......但不确定如何实现这种方式