Implementando o Padrão de Comando em uma API RESTful

12

Estou no processo de projetar uma API HTTP, espero que seja o mais RESTful possível.

Existem algumas ações cuja funcionalidade se espalha por alguns recursos e, às vezes, precisa ser desfeita.

Pensei comigo mesmo: isso parece um padrão de comando, mas como posso modelá-lo em um recurso?

Vou apresentar um novo recurso chamado XXAction, como DepositAction, que será criado através de algo como isto

POST /card/{card-id}/account/{account-id}/Deposit
AmountToDeposit=100, different parameters...

isso criará uma nova DepositAction e ativará seu método Do / Execute. Nesse caso, retornar um status HTTP criado 201 significa que a ação foi executada com sucesso.

Posteriormente, se um cliente desejar examinar os detalhes da ação, ele poderá

GET /action/{action-id}

Update / PUT deve estar bloqueado, eu acho, porque não é relevante aqui.

E para desfazer a ação, pensei em usar

DELETE /action/{action-id}

que na verdade chama o método Undo do objeto relevante e altera seu status.

Digamos que estou feliz com apenas um Do-Undo, não preciso refazer.

Esta abordagem está correta?

Existem armadilhas, razões para não usá-lo?

Isso é entendido pelo ponto de vista dos clientes?

Mithir
fonte
Resposta curta, isso não é REST.
Evan Plaice
3
@EvanPlaice gostaria de elaborar isso? essa é exatamente a questão.
Mithir
1
Eu teria elaborado uma resposta, mas a resposta de Gary já cobre a maioria / tudo o que eu acrescentaria. Eu digo que não é descanso porque os URIs devem representar apenas recursos (ou seja, não ações). As ações são tratadas por meio de GET / POST / PUT / DELETE / HEAD. Pense no REST como uma interface OOP. O objetivo é fazer com que a API se ajuste ao padrão geral e o desacople dos detalhes específicos da implementação quanto possível.
quer
1
@EvanPlaice Ok, entendo, obrigado. Acho que está confundindo aqui porque Depósito poderia ser pensado como um substantivo e como um verbo ...
Mithir
Nesse caso, o URI deve representar uma transação em que debitar (receber dinheiro) e creditar (dar dinheiro) são ações realizadas por meio de solicitações POST. O POST é usado para ambos, porque cada vez que o dinheiro é movido em qualquer direção, representa uma nova transação sendo criada. No seu caso específico, as transações estão ocorrendo na conta do titular do cartão, portanto o número da conta do cartão é o URI do recurso.
Evan Plaice

Respostas:

13

Você está adicionando uma camada de abstração que é confusa

Sua API começa muito limpa e simples. Um HTTP POST cria um novo recurso de depósito com os parâmetros fornecidos. Em seguida, você sai dos trilhos introduzindo a ideia de "ações" que são um detalhe de implementação e não uma parte essencial da API.

Como alternativa, considere esta conversa HTTP ...

POST / cartão / {id do cartão} / conta / {id da conta} / Depósito

AmountToDeposit = 100, parâmetros diferentes ...

201 CRIADO

Localização = / cartão / 123 / conta / 456 / Depósito / 789

Agora você deseja desfazer esta operação (tecnicamente, isso não deve ser permitido em um sistema de contabilidade balanceado, mas o que é necessário):

DELETE / cartão / 123 / conta / 456 / depósito / 789

204 SEM CONTEÚDO

O consumidor da API sabe que está lidando com um recurso de depósito e é capaz de determinar quais operações são permitidas nele (geralmente por meio de OPTIONS em HTTP).

Embora a implementação da operação de exclusão seja realizada por meio de "ações" hoje, não há garantia de que quando você migra esse sistema de, digamos, C # para Haskell e mantém o front-end de que o conceito secundário de uma "ação" continuaria agregando valor , enquanto o conceito principal de depósito certamente o faz.

Edite para cobrir uma alternativa para EXCLUIR e depositar

Para evitar uma operação de exclusão, mas ainda assim remover efetivamente o depósito, você deve fazer o seguinte (usando uma transação genérica para permitir depósito e retirada):

POST / cartão / {ID do cartão} / conta / {ID da conta} / Transação

Valor = -100 , parâmetros diferentes ...

201 CRIADO

Localização = / card / 123 / account / 456 / Transation / 790

Um novo recurso de transação é criado com a quantidade exatamente oposta (-100). Isso tem o efeito de equilibrar a conta de volta para 0, negando a transação original.

Você pode considerar a criação de um terminal "utilitário" como

POST / cartão / {ID do cartão} / conta / {ID da conta} / Transação / 789 / Desfazer <- RUIM!

para obter o mesmo efeito. No entanto, isso quebra a semântica de um URI como identificador, introduzindo um verbo. É melhor aderir aos substantivos nos identificadores e manter as operações restritas aos verbos HTTP. Dessa forma, você pode criar facilmente um link permanente a partir do identificador e usá-lo para GETs e assim por diante.

Gary Rowe
fonte
3
+1 "tecnicamente, isso não deve ser permitido em um sistema de contabilidade equilibrado". Alguém sabe contar feijões. Essa afirmação é absolutamente correta, a maneira de reverter seria criar outra transação creditando os fundos de volta. As entradas do Razão sempre devem ser consideradas imutáveis ​​e permanentes após a conclusão de uma transação.
Evan Plaice
Então, se eu mudar, nas minhas perguntas, em vez de Excluir / ação / ... para Excluir / depositar / ... tudo bem?
Mithir
2
@Mithir eu estava descrevendo a regra contábil. Em um sistema contábil padrão de entrada dupla, você nunca remove transações. A história, uma vez comprometida, é considerada imutável para manter as pessoas honestas. No seu caso, você ainda pode usar uma ação DELETE, mas no back-end (tabela do banco de dados de contabilidade geral) você adiciona outra transação que representa creditar (ou seja, devolver) o dinheiro de volta ao usuário. Eu não sou contador de grãos (ou seja, contador), mas é uma das práticas padrão ensinadas em um curso "Princípios de contabilidade I".
Evan Solha
2
(cont.) Os logs do banco de dados usam transações de maneira semelhante. É por isso que é possível replicar e / ou reconstruir um conjunto de dados usando apenas os logs. Desde que as transações sejam reproduzidas cronologicamente, deve ser possível reconstruir o conjunto de dados a partir de qualquer ponto do histórico. Remover a mutabilidade da equação garante consistência.
Evan Solha
1
É justo renomeá-lo para Transação.
Gary Rowe
1

O principal motivo da existência do REST é a resiliência contra erros de rede. Para que fim todas as operações devem ser idempotentes .

A abordagem básica parece razoável, mas a maneira como você descreve a DepositActioncriação não parece ser idempotente, o que deve ser corrigido. Ao fazer com que o cliente forneça um ID exclusivo que será usado para detectar solicitações duplicadas. Então a criação mudaria para

PUT /card/{card-id}/account/{account-id}/Deposit/{action-id}
AmountToDeposit=100, different parameters...

Se outro PUT para o mesmo URL for feito com o mesmo conteúdo que anteriormente, a resposta ainda será 201 createdse o conteúdo for o mesmo e erro se o conteúdo for diferente. Isso permite ao cliente simplesmente retransmitir a solicitação quando falha, pois não pode dizer se a solicitação ou resposta foi perdida.

Faz mais sentido usar o PUT, porque ele apenas grava o recurso e é idempotente, mas o uso do POST também não causaria nenhum problema.

Para examinar os detalhes da transação, o cliente terá GETa mesma URL, ou seja,

GET /card/{card-id}/account/{account-id}/Deposit/{action-id}

e para desfazê-lo, ele pode EXCLUÍ-LO. Mas se ele realmente tem alguma coisa a ver com dinheiro, como sugere a amostra, sugiro COLOCÁ-LO com sinalizadores "cancelados" adicionados, em vez de prestar contas (que ainda resta vestígios de transações criadas e canceladas).

Agora você precisa escolher um método para criar o ID exclusivo. Você tem várias opções:

  1. Emita o prefixo específico do cliente anteriormente na troca que deve ser incluído.
  2. Adicione uma solicitação POST especial para obter um ID exclusivo em branco do servidor. Essa solicitação não precisa ser idempotente (e realmente não pode), porque os IDs não utilizados não causam nenhum problema.
  3. Basta usar o UUID. Todo mundo usa e ninguém parece ter nenhum problema com os baseados em MAC nem os aleatórios.
Jan Hudec
fonte
2
Pelo que eu sei, o POST não é idempotente. en.wikipedia.org/wiki/POST_(HTTP)#Affecting_server_state
Mithir
@Mithir: POST não é considerado idempotente; ainda pode ser. Mas é verdade que, como todas as operações REST devem ser idempotentes, o POST basicamente não tem lugar no REST.
Jan Hudec
1
Estou confuso ... o conteúdo que li e a implementação existente em que estou familiarizado (ServiceStack, ASP.NET Web API), tudo sugere que o POST tem um lugar no REST.
Mithir
3
No REST, a idempotência é atribuída ao recurso, não ao protocolo ou seus códigos de resposta. Assim, no REST sobre HTTP, os métodos GET, PUT, DELETE, PATCH e assim por diante são considerados idempotentes, embora seus códigos de resposta possam variar para chamadas subseqüentes. O POST é idempotente no sentido de que toda chamada cria um novo recurso. Consulte Fielding. Não há problema em usar o POST .
Gary Rowe
1
Operações que não são idempotentes são permitidas em repouso. Essa afirmação é totalmente errada.
Andy