RESTFul: ações de alteração de estado

59

Estou planejando criar a API RESTfull, mas há algumas questões de arquitetura que estão criando alguns problemas na minha cabeça. Adicionar lógica de negócios de back-end aos clientes é uma opção que eu gostaria de evitar, pois é difícil manter a atualização de várias plataformas de clientes em tempo real, quando a lógica de negócios pode mudar rapidamente.

Digamos que tenhamos artigo como recurso (API / artigo), como devemos implementar ações como publicar, cancelar publicação, ativar ou desativar e assim por diante, mas para tentar mantê-lo o mais simples possível?

1) Devemos usar api / article / {id} / {action}, já que muita lógica de back-end pode acontecer lá, como enviar para locais remotos ou alterar várias propriedades. Provavelmente, a coisa mais difícil aqui é que precisamos enviar todos os dados do artigo de volta à API para atualização e o trabalho multiusuário não pôde ser implementado. Por exemplo, o editor poderia enviar dados mais antigos de 5 segundos e substituir a correção que algum outro jornalista fez há apenas 2 segundos atrás, e não há como explicar isso aos clientes, uma vez que aqueles que publicam um artigo não estão realmente conectados à atualização do conteúdo.

2) Criar novo recurso também pode ser uma opção, api / article- {action} / id, mas o recurso retornado não seria article- {action}, mas artigo que não tenho certeza se isso é apropriado. Também na classe de artigo de código do lado do servidor está lidando com o trabalho real nos dois recursos e não tenho certeza se isso vai contra o pensamento REST

Todas as sugestões são bem-vindas ..

Miro Svrtan
fonte
É perfeitamente legal que 'ações' façam parte de um URI RESTful - se elas indicarem uma ação / algoritmo a ser executado. O que há de errado api/article?action=publish? Os parâmetros de consulta destinam-se a casos em que o estado do recurso depende do 'algoritmo' (ou ação) mencionado. Por exemplo, api/articles?sort=ascé válido
PhD
11
Sugiro que você verifique este artigo, o que pode inspirá-lo com uma solução RESTful ainda mais .
Benjol 26/03/12
Um dos problemas que vejo com api / article? Action = publish é que no aplicativo RESTfull deve enviar TODOS os dados do artigo para publicação, enquanto eu preferiria fazer exatamente isso: api / article / 4545 / publish / sem nada adicional
Miro Svrtan
2
Excelente recurso sobre esta questão e mais: API Design Resto - Resource Modeling
Izhaki
@ Phd: embora seja perfeitamente legal no protocolo, os URIs geralmente devem ser substantivos, e não verbos. Ter um verbo no URI geralmente é um sinal de mau design do REST.
Lie Ryan

Respostas:

49

As práticas descritas aqui são úteis:

E as ações que não se encaixam no mundo das operações CRUD?

É aqui que as coisas podem ficar confusas. Existem várias abordagens:

  1. Reestruture a ação para aparecer como um campo de um recurso. Isso funciona se a ação não aceitar parâmetros. Por exemplo, uma ação de ativação pode ser mapeada para um activatedcampo booleano e atualizada via PATCH para o recurso.
  2. Trate-o como um sub-recurso com princípios RESTful. Por exemplo, a API do GitHub permite estrelar uma essência com PUT /gists/:id/stare Remover a estrela com DELETE /gists/:id/star.
  3. Às vezes, você realmente não tem como mapear a ação para uma estrutura RESTful sensível. Por exemplo, uma pesquisa com vários recursos realmente não faz sentido para ser aplicada ao terminal de um recurso específico. Nesse caso, /searchfaria mais sentido, mesmo que não seja um recurso. Tudo bem - faça o que é certo da perspectiva do consumidor da API e verifique se está documentado claramente para evitar confusão.
Tim
fonte
Eu voto na abordagem 2. Embora pareça recursos artificiais desajeitados para os chamadores de API, na realidade eles não sabem o que está acontecendo no servidor. Se eu ligar /article/123/deactivationspara o POST para criar uma nova solicitação de desativação para o artigo 123, o servidor poderá não apenas desativar o recurso solicitado, mas também armazenar minha solicitação de desativação para recuperar seu status posteriormente.
25416 justamartin
2
por que PUT /gists/:id/star não POST /gists/:id/star?
Filip Bartuzi
9
@FilipBartuzi Como PUT é idempotente - ou seja, não importa quantas vezes você execute uma ação com os mesmos parâmetros, o resultado será sempre o mesmo (por exemplo, se você desligar uma luz, ela muda. Se você tentar ligar novamente, nada muda - já está ativado). O POST não é idempotente - ou seja, toda vez que você executa uma ação, mesmo com os mesmos parâmetros, a ação tem um resultado diferente (por exemplo, se você enviar uma carta para alguém, essa pessoa recebeu uma carta. Se você enviar uma carta idêntica para a mesma pessoa, eles agora têm 2 letras).
Raphael
9

Operações que resultam em grandes mudanças de estado e comportamento no lado do servidor, como a ação "publicar" que você descreve, são difíceis de modelar explicitamente no REST. Uma solução que vejo frequentemente é conduzir comportamentos tão complexos implicitamente através dos dados.

Considere fazer o pedido de mercadorias por meio de uma API REST exposta por um comerciante on-line. Encomendar é uma operação complexa. Vários produtos serão embalados e enviados, sua conta será cobrada e você receberá um recibo. Você pode cancelar seu pedido por um período limitado e, é claro, há uma garantia de devolução total do dinheiro, que permite o envio de produtos para reembolso.

Em vez de uma operação de compra complexa, essa API pode permitir que você crie um novo recurso, um pedido de compra. No início, você pode fazer as modificações desejadas: adicionar ou remover produtos, alterar o endereço de entrega, escolher outra opção de pagamento ou cancelar seu pedido completamente. Você pode fazer tudo isso porque ainda não comprou nada, está apenas manipulando alguns dados no servidor.

Depois que seu pedido de compra é concluído e seu período de carência passa, o servidor bloqueia seu pedido para evitar outras alterações. Somente nesse momento a sequência complexa de operações é iniciada, mas você não pode controlá-la diretamente, apenas indiretamente através dos dados inseridos anteriormente no pedido.

Com base na sua descrição, "publicar" pode ser implementado dessa maneira. Em vez de expor uma operação, você coloca uma cópia do rascunho que analisou e deseja publicar como um novo recurso em / publicar. Isso garante que as atualizações subseqüentes ao rascunho não serão publicadas, mesmo que a operação de publicação seja concluída horas depois.

Ferenc Mihaly
fonte
A idéia de alterar todo o recurso de artigo não publicado para rascunho se encaixaria exatamente nesse caso, mas não se encaixaria em todas as outras ações que existem em um recurso em geral. O REST é capaz de lidar com isso? Talvez eu esteja abusando dele e deva usá-lo apenas como CRUD e nada mais, como consultas SQL em que não espero que nenhuma lógica esteja dentro da própria consulta.
Miro Svrtan
Por que estou perguntando tudo isso? Bem, já que há algum tempo, os aplicativos da Web começaram a ser multiplataforma e eu preferia manter muita lógica de negócios no servidor, já que a atualização da lógica de negócios no iOS, Android, Web, desktop ou qualquer outra plataforma que vem à mente está ficando praticamente impossível de ser feita. rapidamente e gostaria de evitar todos os problemas de compatibilidade com versões anteriores ao alterar um pequeno pedaço de BL.
Miro Svrtan
2
Acho que o REST pode lidar com a lógica de negócios muito bem, mas não é adequado para expor a lógica de negócios existente escrita sem ter o REST em mente. É por isso que muitas empresas como Microsoft, SAP e outras frequentemente expõem apenas dados com operações CRUD, como você disse. Consulte o Protocolo de dados abertos para ver como eles o fazem.
Ferenc Mihaly 25/03
7

precisamos enviar todos os dados do artigo de volta à API para atualização e o trabalho multiusuário não pôde ser implementado. Por exemplo, o editor poderia enviar dados mais antigos de 5 segundos e substituir a correção que algum outro jornalista fez há apenas 2 segundos atrás, e não há como explicar isso aos clientes, uma vez que aqueles que publicam um artigo não estão realmente conectados à atualização do conteúdo.

Esse tipo de coisa é um desafio, não importa o que você faça. É um problema muito semelhante ao controle de fonte distribuído (mercurial, git etc.), e a solução, escrita em HTTP / ReST, parece um pouco semelhante.

Supondo que você tenha dois usuários, Alice e Bob, ambos trabalhando /articles/lunch. (para maior clareza, a resposta está em negrito)

Primeiro, Alice cria o artigo.

PUT /articles/lunch HTTP/1.1
Host: example.com
Content-Type: text/plain
Authorization: Basic YWxpY2U6c2VjcmV0

Hey Bob, what do you want for lunch today?

301 Moved Permanently
Location: /articles/lunch/1 

O servidor não criou um recurso, porque não havia "versão" anexada à solicitação (assumindo um identificador de /articles/{id}/{version}. Para executar a criação, Alice foi redirecionada para o URL do artigo / versão que criará. O usuário de Alice O agente reaplicará a solicitação no novo endereço.

PUT /articles/lunch/1 HTTP/1.1
Host: example.com
Content-Type: text/plain
Authorization: Basic YWxpY2U6c2VjcmV0

Hey Bob, what do you want for lunch today?

201 Created

E agora o artigo foi criado. Em seguida, Bob analisa o artigo:

GET /articles/lunch HTTP/1.1
Host: example.com
Authorization: Basic Ym9iOnBhc3N3b3Jk

301 Moved Permanently
Location: /articles/lunch/1 

Bob olha para lá:

GET /articles/lunch/1 HTTP/1.1
Host: example.com
Authorization: Basic Ym9iOnBhc3N3b3Jk

200 Ok
Content-Type: text/plain

Hey Bob, what do you want for lunch today?

Ele decide adicionar sua própria mudança.

PUT /articles/lunch/1 HTTP/1.1
Host: example.com
Content-Type: text/plain
Authorization: Basic Ym9iOnBhc3N3b3Jk

Hey Bob, what do you want for lunch today?
Does pizza sound good to you, Alice?

301 Moved Permanently
Location: /articles/lunch/2

Assim como Alice, Bob é redirecionado para onde ele criará uma nova versão.

PUT /articles/lunch/2 HTTP/1.1
Host: example.com
Content-Type: text/plain
Authorization: Basic Ym9iOnBhc3N3b3Jk

Hey Bob, what do you want for lunch today?
Does pizza sound good to you, Alice?

201 Created

Por fim, Alice decide que gostaria de adicionar ao seu próprio artigo:

PUT /articles/lunch/1 HTTP/1.1
Host: example.com
Content-Type: text/plain
Authorization: Basic YWxpY2U6c2VjcmV0

Hey Bob, what do you want for lunch today?
I was thinking about getting Sushi.

409 Conflict
Location: /articles/lunch/3
Content-Type: text/diff

---/articles/lunch/2
+++/articles/lunch/3
@@ 1,2 1,2 @@
 Hey Bob, what do you want for lunch today?
-Does pizza sound good to you, Alice?
+I was thinking about getting Sushi.

Em vez de ser redirecionado normalmente, um código de status diferente é retornado ao cliente 409, que informa a Alice que a versão da qual ela estava tentando ramificar já foi ramificada. Os novos recursos foram criados de qualquer maneira (conforme mostrado no Locationcabeçalho) e as diferenças entre os dois foram incluídas no corpo da resposta. Alice agora sabe que o pedido que ela acabou de fazer precisa ser mesclado de alguma maneira.


Todo esse redirecionamento está relacionado à semântica de PUT, o que exige que novos recursos sejam criados exatamente onde a linha de solicitação solicita. isso também poderia salvar um ciclo de solicitação usando POST, mas o número da versão teria que ser codificado na solicitação por alguma outra mágica, o que me pareceu menos óbvio para fins de ilustração, mas provavelmente ainda seria preferido em uma API real para minimizar os ciclos de solicitação / resposta.

SingleNegationElimination
fonte
11
Versoning não era um problema aqui, eu só disse isso como exemplo de possíveis problemas se usar o artigo como um recurso para a ação de publicação
Miro Svrtan
3

Aqui está outro exemplo que lida não com o conteúdo dos documentos, mas mais com o estado transitório. (Acho que a versão - já que, em geral, cada versão pode ser um novo recurso - é um tipo de problema fácil.)

Digamos que eu queira expor um serviço em execução em uma máquina por meio de um REST para que ele possa ser parado, iniciado, reiniciado e assim por diante.

Qual é a abordagem mais RESTful aqui? POST / service? Command = restart, por exemplo? Ou POST / service / state com um corpo de, digamos, 'executando'?

Seria bom codificar as práticas recomendadas aqui e se o REST é a abordagem correta para esse tipo de situação.

Segundo, suponha que eu queira conduzir alguma ação de um serviço que não afeta seu próprio estado, mas desencadeia um efeito colateral. Por exemplo, um serviço de mala direta que envia um relatório, construído no momento da chamada, para vários endereços de email.

GET / report pode ser uma maneira de obter uma cópia do relatório; mas e se quisermos enviar para o lado do servidor outras ações, como enviar e-mail, como eu disse acima? Ou escrevendo em um banco de dados.

Esses casos dançam em torno da divisão de recursos e ações, e vejo maneiras de lidar com eles de uma maneira orientada a REST, mas, francamente, parece um hack para isso. Talvez a questão principal seja se uma API REST deve suportar efeitos colaterais em geral.

AMD
fonte
2

O REST é orientado a dados e, como tais recursos, funciona melhor como "coisas" e não ações. A semântica implícita dos métodos http; GET, PUT, DELETE, etc servem para reforçar a orientação. POST, é claro, é a exceção.

Um recurso pode ser uma mistura de dados, ou seja. conteúdo do artigo; e metadados, ie. publicado, bloqueado, revisão. Existem muitas outras maneiras possíveis de dividir os dados, mas é necessário analisar primeiro como será o fluxo de dados para determinar a melhor opção (se houver). Por exemplo, pode ser que as revisões sejam seus próprios recursos no artigo, como sugere o TokenMacGuy.

Em relação à implementação, eu provavelmente faria algo como o que o TockenMacGuy sugere. Eu também adicionaria campos de metadados no artigo, não na revisão, como 'bloqueado' e 'publicado'.

dietbuddha
fonte
1

Não pense nisso como uma manipulação direta do estado do artigo. Em vez disso, você está solicitando uma alteração solicitando a criação do artigo.

Você pode modelar a colocação de uma requisição de mudança como criando um novo recurso de requisição de mudança (POST). Há muitas vantagens. Por exemplo, você pode especificar uma data e hora futuras em que o artigo deve ser publicado como parte da requisição de mudança e deixar o servidor se preocupar com a implementação.

Se a publicação não for um processo instantâneo, não será necessário aguardar o término antes de retornar ao cliente. Você apenas reconhece que a requisição de mudança foi criada e retorna o ID da requisição de mudança. Você pode usar o URL correspondente a essa requisição de mudança para compartilhar o status da requisição de mudança.

Um insight importante para mim foi reconhecer essa metáfora da requisição de mudança é apenas outra maneira de descrever a programação orientada a objetos. Em vez de recursos, chamamos objetos. Em vez de requisições de mudança, as chamamos de mensagens. Uma maneira de enviar uma mensagem de A para B no OO é fazer com que A chame um método em B. Outra maneira de fazer isso, principalmente quando A e B estão em computadores diferentes, é fazer com que A crie um novo objeto, M e envie para B. O REST simplesmente formaliza esse processo.

Patrick McElhaney
fonte
Na verdade, eu consideraria isso mais próximo do modelo do ator do que do OO. Considerar solicitações REST como Mensagens (que são) as alinha muito bem ao Event Sourcing para gerenciar o estado. O REST divide ordenadamente suas interações ao longo das linhas do CQRS. As mensagens GET são: Query, POST, PUT, PATCH, DELETE na área de comando.
Willd
0

Se o entendi corretamente, acho que você tem mais um problema de determinação de 'regra de negócios' do que um problema técnico.

O fato de um artigo poder ser substituído pode ser resolvido com a introdução de níveis de autorização nos quais usuários seniores podem substituir as versões de usuário júnior. Também com a introdução de versões e de uma coluna para capturar o estado do artigo (por exemplo, 'em desenvolvimento', 'final' , etc.), você pode superar isso. Você também pode dar ao usuário a capacidade de selecionar uma versão específica combinando o tempo de envio e o número da versão.

Em todos os casos acima, seu serviço precisa implementar as regras de negócios definidas por você. Portanto, você pode chamar o serviço com os parâmetros: ID do usuário, artigo, versão, ação (onde a versão é opcional, novamente isso depende das regras de negócios).

NoChance
fonte
Não acredito que essa seja uma regra de negócios, mas estritamente técnica. A idéia de adicionar versão é boa para ajudar a substituir regras, mas ainda não resolve a situação em que atualizar conteúdo e publicar conteúdo não são ações relacionadas.
Miro Svrtan