Projetando API RESTful baseada em função

8

Por favor, acerte uma discussão entre mim e um amigo.

No momento, estamos projetando uma API do produto. Nossa entidade de produto se parece com isso

{
    "Id": "",
    "ProductName": "",
    "StockQuantity": 0
}

As vendas dos produtos são realizadas por terceiros e são obrigadas a nos informar a quantidade comprada para que o StockQuantitycampo possa ser diminuído.

Minha abordagem:

PUT /api/Product/{Id}/ --data { "StockQuantity": "{NewStockQuantity}" }

O terceiro é responsável por consultar o produto, fazer o cálculo com base na StockQuantityquantidade atual e adquirida e enviar uma PUTsolicitação com o novo valor.

Meu amigo não quer que terceiros façam o cálculo. A abordagem dele

PUT /api/Product/{Id}/DecreaseStock --data { "PurchasedQuantity": "{PurchasedQuantity}" }

Para que possamos fazer o cálculo e atualizar o StockQuantity

Não quero criar pontos de extremidade baseados em funções e ele não deseja confiar em terceiros para fazer os cálculos.

Qual seria a maneira correta de abordar esse problema?

Sefa Ümit Oray
fonte
Lembre-se de que o PUT deve ser (em teoria) idempotente. A opção 1 caberia na semântica. A opção 2 não. Se é compatível com REST, é importante para você. Você tentaria manter as chamadas PUT idempotentes, pois ele evitará muitas dores de cabeça. O mesmo para DELETE. Para operações tipo comando, honestamente, eu daria uma chance ao Json ou XML RPC. Ambas as estratégias (REST e RPC) podem viver juntas na mesma API da Web. Ela transmite com o princípio da CQRS (comando responsabilidade consulta segregação :-)
LAIV

Respostas:

19

Você pode permitir que seus terceiros publiquem o seu produto. Por exemplo:

POST /product/{id}/sale { "Quantity": 3 }

Concordo com o seu ponto e com o do seu colega. Essa é a lógica de negócios e não deve ser deixada para o cliente da API, mas você também deve evitar ter "funções" como pontos de extremidade.

Às vezes, resolver esses problemas é tão fácil quanto chamá-lo de maneira diferente, reconhecidamente nem sempre.

Robert Bräutigam
fonte
2
Este. Mais: parece que cada venda também precisa de um objeto no banco de dados. Ter cada venda como objeto separado no db permite a rastreabilidade. Pense, se algo der errado e a quantidade final de estoque estiver errada e precisar fixar valores. Se você tiver apenas uma coluna do valor final, não poderá fazer muito. Esperançosamente, existem logs úteis no sistema para descobrir o que deu errado. Se você tiver objetos de venda com registro de data e hora, nomes de usuário e possivelmente endereço IP anexado, poderá excluir determinados registros para corrigir dados e rastrear de qual usuário / local ele veio.
Ski
Obrigado pelas contribuições. Venda / Pedido é recurso de outra equipe, não é minha responsabilidade salvá-los ou processá-los. Sabe disso, a criação de /saleterminal ainda é válida?
Sefa Ümit Oray
@ SefaÜmitOray: Os pontos de extremidade /salee /product/{id}/salesão completamente independentes e o fato de terem nomes semelhantes não implica, de forma alguma, que eles se refiram ao mesmo recurso.
Bart van Ingen Schenau
@BartvanIngenSchenau O que quero dizer é que salenão está no meu domínio e não faz parte product. Ainda faz sentido criar /product/{id}/saleenquanto não representa nenhum recurso real?
Sefa Ümit Oray
5
@ SefaÜmitOray É completamente válido se representar algo significativo no seu contexto. Ele não precisa significar a mesma coisa que em outros contextos e também não precisa ser algo que persista diretamente no banco de dados. Domínio! = Tabelas de banco de dados, Recurso! = Tabelas de banco de dados.
Robert Bräutigam
3

Não há razão para que você também não possa fazer; ou ambos.

Em um contexto de ponto de venda, o rastreamento de transações individuais faz muito sentido. Lá, a solução de Robert faz muito sentido.

No contexto de estoque / armazém, você não controla necessariamente as transações, mas "faz o inventário"; ter um terminal que permita ao cliente relatar seus níveis de estoque

Tenho 10 unidades tenho 7 unidades tenho 3 unidades tenho 20 unidades

faz muito sentido.

Os níveis de estoque mudam por outros motivos que não "vendas"; Apenas algo para ter em mente.

Em teoria, o nível de estoque deve ser computável a partir das mudanças; mas em alguns domínios, essa é precisamente a suposição que você deseja verificar . Você deseja calcular o nível de estoque de duas maneiras diferentes e verificar discrepâncias (também conhecido como "encolhimento").

Portanto, não acho que a semântica seja clara, com base no contexto que você forneceu.

Quanto à parte HTTP; PUT [target-uri]faz sentido semanticamente quando você está substituindo uma representação de um documento por outra. É um UPSERT- o segundo PUT para um recurso está pedindo para substituir a representação existente.

PUT /sales { Quantity = 5 }
PUT /sales { Quantity = 2 }
PUT /sales { Quantity = 3 }

diz que a quantidade de unidades vendidas 3não é 10.

PUT /sales/1 { Quantity = 5 }
PUT /sales/2 { Quantity = 2 }
PUT /sales/3 { Quantity = 3 }

É assim que 10parece

PUT /sales { Quantity : [5] }
PUT /sales { Quantity : [5,2] }
PUT /sales { Quantity : [5,2,3] }

Essa é outra maneira de ortografia 10.

POST /sales { Quantity = 5 }
POST /sales { Quantity = 2 }
POST /sales { Quantity = 3 }

No que diz respeito ao HTTP, isso também é aceitável. No entanto, não é uma ótima opção em uma rede não confiável porque as mensagens às vezes são duplicadas.

POST /sales { Quantity = 5 }
POST /sales { Quantity = 2 }
POST /sales { Quantity = 3 }
POST /sales { Quantity = 3 }

É isso 13? ou 10?

PUT /sales/1 { Quantity = 5 }
PUT /sales/2 { Quantity = 2 }
PUT /sales/3 { Quantity = 3 }
PUT /sales/3 { Quantity = 3 }

Isso é inequivocamente 10

PUT /sales { Quantity : [5,2,3] }
PUT /sales { Quantity : [5,2,3] }

Isso é inequivocamente 10

PUT /sales/1 { Quantity = 5 }
PUT /sales/2 { Quantity = 2 }
PUT /sales/3 { Quantity = 3 }
PUT /sales/4 { Quantity = 3 }

Isso é inequivocamente 13

PUT /sales { Quantity : [5,2,3] }
PUT /sales { Quantity : [5,2,3,3] }

Isso é inequivocamente 13

POST /sales { TransactionId = 1 , Quantity = 5 }
POST /sales { TransactionId = 2 , Quantity = 2 }
POST /sales { TransactionId = 3 , Quantity = 3 }
POST /sales { TransactionId = 3 , Quantity = 3 }

10

POST /sales { TransactionId = 1 , Quantity = 5 }
POST /sales { TransactionId = 2 , Quantity = 2 }
POST /sales { TransactionId = 3 , Quantity = 3 }
POST /sales { TransactionId = 4 , Quantity = 3 }

13

(Para ser justo, o HTTP tem suporte para solicitações condicionais ; você pode elevar alguns dos metadados do protocolo específico do domínio para os cabeçalhos agnósticos do domínio para eliminar parte da ambiguidade - se conseguir convencer o cliente a seguir adiante).

Obviamente, existem trocas - o HTML não tem suporte nativo a PUT; se você pretende que os clientes da sua API sejam navegadores, é necessário um protocolo baseado no POST ou extensões de código sob demanda para converter o envio do formulário de um POST para um PUT.

VoiceOfUnreason
fonte
1
Você não precisa acompanhar as vendas individuais, apenas porque existe um ponto final para isso. Ou seja, não há necessidade de listar as chamadas de venda anteriores, apenas porque você pode fazer o POST. No entanto, você está certo de que pode haver outros casos de uso (não sabemos) e deve definir chamadas idempotentes com chamadas condicionais ou com outros meios.
Robert Bräutigam
2

Parece um design muito ruim, não importa como você o corta. Eu nunca confiaria a terceiros para me informar o inventário atual, a menos que eu os contratasse para gerenciar meu armazém.

Além disso, a abordagem de aparência de função não é RESTful e deve criar consternação entre seus consumidores.

Por fim, não consigo imaginar um cenário em que a única coisa que você se preocupa com uma venda é o inventário resultante que resta depois que ela é concluída.

É muito melhor que terceiros publiquem um recurso de Venda ou Fatura para você (com informações sobre qual produto, quantidade, data, método de envio, informações do cliente etc.). Isso permite que você faça análises e rastreamentos reais do que você está vendendo, para quem, quando, etc., para que você possa realmente planejar seus negócios.

Mesmo se seu terceiro estiver realizando o atendimento total do pedido, você desejará acompanhar as vendas para fins contábeis e demográficos dos clientes, se nada mais.

Paulo
fonte
1

PUT / api / Product / {ID} / --dados {"StockQuantity": "{NewStockQuantity}"}

Esse tipo de design tem um problema importante: se você quiser ter mais de um encadeamento de cliente em execução na sua API, estará sujeito a leituras / gravações sujas. Ou seja, entre o momento em que o cliente reduz a quantidade atual e calcula o novo valor, outro cliente pode obter o mesmo valor anterior e calcular uma resposta diferente. A quantidade que você terminar será a que for atualizada por último, mas nenhuma estiver correta. Por exemplo, digamos que sua quantidade atual seja 10. O cliente A deseja vender 5 itens e extrai a quantidade atual. Ao mesmo tempo, o cliente B deseja vender 6 itens e extrai a quantidade atual. Ambos vêem 10 itens em estoque. A calcula 5 itens restantes. Bcalcula 4 restantes. Ambos são atualizados. Agora você mostra 4 ou 5 itens restantes, dependendo de quem foi a última atualização registrada. No entanto, você realmente vendeu mais itens do que realmente possui. O pior é que não há maneira fácil de percorrer e ver o que deu errado. Tudo o que você tem são dois incorretos PUTsem seus logs para examinar.

Em qualquer sistema de registro do mundo real, simplesmente ter um total atual não é adequado. Considere se você for a uma loja e comprar vários itens. Você pede um recibo e o caixa apenas entrega um recibo com um único total. Como você mostraria que o total está correto com esse recibo? Como você mostraria que comprou um item se quisesse devolver algo?

A abordagem do seu amigo é melhor, mas eu sugiro adicionar um ID de transação ao mix. Isso aborda as preocupações reais mencionadas pelo VoiceOfUnreason sobre transações duplicadas. Uma opção é fornecer uma POSToperação para criar uma nova transação e, em seguida, PUTconfirmar a transação. No momento da confirmação, você reduz o estoque total ou nega a solicitação, porque não há disponibilidade suficiente.

JimmyJames
fonte
1

Como as vendas são realizadas por terceiros, você precisa ter controle sobre o estoque do produto, não permitindo que eles atualizem a contagem de estoque.

Para uso interno, por exemplo, para fins de contagem de estoque, você pode ter sua abordagem, por exemplo PUT /api/Product/{Id}/ --data { "StockQuantity": "{NewStockQuantity}" }.

Para uso externo, é necessário criar uma interface separada, por exemplo, /api/SalesOrder/que obtenha uma lista de produtos e quantidades, como:

POST /api/SalesOrder/ --data { [{"Id": 1, "Qty": 1}, {"Id": 2, "Qty": 3}] }

Com base no SalesOrderenvio de terceiros, a quantidade de cada produto pode ser atualizada e atribuída ao pedido ou você pode rejeitá-lo se não houver produto suficiente disponível.

O processamento e a contagem de estoques são processos internos. Terceiros exigem apenas interface para que possam encaminhar seus pedidos ao estoque. Basicamente, SalesOrderé assim que as Vendas, Finanças e Armazém se comunicam para concluir uma venda.

imel96
fonte