Farei essa pergunta dessa maneira - quais são as preocupações de engenharia de software por não implementar minha API REST da maneira "correta"?
O que você quer dizer com o caminho "certo"? Bem, permita-me explicar minha percepção do caminho certo, depois eu direi como estou fazendo isso (também, assuma que estou falando de uma API JSON REST).
O caminho certo
Apatridia. Esta é a parte que recebo. O cliente mantém o estado sempre 100% do tempo para sempre. Não é o trabalho do servidor, é o cliente.
As ações e respostas esperadas para cada verbo:
- GET - Obtém um recurso especificado na íntegra, limitado apenas pela autorização na solicitação ou por um parâmetro de consulta. Isso garante nenhuma modificação de qualquer recurso no processo.
- POST - Dada uma descrição completa do recurso (como um objeto JSON), cria um recurso e, em seguida, retorna esse recurso, com todas as propriedades do servidor também criadas, como datas ou IDs.
- DELETE - Exclui um recurso especificado, fornecendo apenas 200 OK como resposta
- PUT - Dada uma declaração de objeto inteira como entrada, atualiza o recurso em um local específico, atualizando todos os campos do recurso para cada um dos campos fornecidos na entrada. Para ficar claro, isso espera que todo o objeto seja passado como entrada. Todo o recurso atualizado é retornado, com todos os campos (de acordo com a autorização ou qualquer outro sinalizador de entrada).
- PATCH - Dado apenas os campos que desejam ser modificados para um recurso, atualiza apenas os campos em um recurso especificado que são fornecidos como entrada. (Aqui é onde não estou claro): O recurso inteiro é retornado? (Ou são apenas os campos atualizados? Não sei. Não ligue.)
- Caminhos de recursos. Dado o relacionamento dos recursos entre si, um caminho de recurso pode se parecer com um dos seguintes:
- / parentresource /: id
- / parentresource /: id / childresource
- / parentresource /: id / childresource /: childId
- / parentresource /: id / childresource /: childId / subresource /: subresourceId (neste exemplo, um subresource pertence a um childresource, que pertence a um recurso pai).
Do jeito que eu quero fazer
A descrição acima é minha compreensão de como uma API REST deve funcionar. Agora, deixe-me listar algumas das minhas variações para o acima:
- PUT / PATCH - Qual o sentido de passar todo o recurso para modificação? Uso apenas PUTs para modificar recursos e passo apenas nos campos que quero atualizar. Como resultado, não preciso usar PATCH
Caminhos de recursos - eu uso GUIDs no meu aplicativo. Como resultado, eles serão globalmente únicos. Por que preciso do caminho completo dos recursos, incluindo os recursos pai, se posso apenas me referir exclusivamente a um sub-recurso? Como:
/ subresource /: subresourceId
Se eu fizesse isso da maneira "correta", tentar fazer referência ao subresource exigiria um caminho completo como:
/ parentresource /: id / childresource /: childId / subresource /: subresourceId
É tudo o que é necessário ? Porque agora eu tenho que ter um tratamento de erro adicional se meu caminho contiver um: subresourceId que não seja realmente de propriedade de um determinado: childId e o mesmo para um: childId que não seja de propriedade de um pai: id. Meu servidor está cuidando da autorização de recursos. Não posso apenas referenciar o recurso em si, e não o caminho completo?A resposta de retorno. Digamos, por exemplo, que minha estrutura de dados é uma árvore hierárquica, sem limites práticos na profundidade da árvore. Os recursos estão em diferentes níveis abaixo da árvore, de maneira hierárquica.
- O GET é óbvio. Se eu receber essa árvore inteira, espero que a árvore inteira seja uma resposta, com os recursos contidos nos recursos.
- Se eu POST para criar um novo recurso, PUT para atualizar ou DELETE para remover, desejo ver os deltas na árvore, em vez de apenas ver o recurso que eu criei / atualizei / excluí. Não quero ter que chamar novamente o GET da árvore pai após cada POST, PUT ou DELETE, especialmente se houver pequenas alterações na árvore e eu apenas desejar ver os deltas.
Espero que minhas perguntas sejam claras.
Se você visse uma implementação REST como a descrevi, você a observaria e me contaria suas preocupações de engenharia de software? Se sim, quais seriam?
Respostas:
A resposta geral subjacente aqui é que suas idéias podem funcionar em um nível técnico, mas isso não significa que estejam em conformidade com as convenções padronizadas do REST.
Sua ideia funciona em nível técnico, mas simplesmente não é como o REST foi descrito. Lembre-se de que qualquer discussão sobre código de trabalho (ou seja, nenhum erro de compilação ou tempo de execução) sempre será uma questão de convenção , não necessariamente de clara superioridade técnica.
Existem muitas nuances sobre como definimos entidades "filho / pai". Mais comumente, refere-se a um relacionamento um para muitos (pai para filhos).
No entanto, suspeito que, para o REST, parte do que faz de uma criança uma criança é a expectativa de que ela só possa acessá-la através dos pais, de que eles não possuam seu próprio identificador globalmente exclusivo (e conhecido externamente).
Suspeito que isso esteja seguindo a mesma filosofia (mas não necessariamente pelo mesmo motivo) que a dos agregados (e suas raízes) no Desenvolvimento Orientado a Domínios .
No seu caso, o que você chama de "pai" funciona como a raiz agregada. O ponto de contato único (se você desejar) para chamadas externas.
Você pode concluir que seu filho é realmente um agregado diferente. Pode ser esse o caso, mas quero emitir um aviso com essa decisão. Você não deve basear sua arquitetura no tipo específico de um campo. Você não tem como saber se sempre usará IDs exclusivos globalmente para todas as suas entidades. Se isso mudar, por qualquer motivo, você comprometerá a viabilidade de sua arquitetura REST; pois você pode acabar em uma situação em que a criança não é mais identificável exclusivamente e, portanto, precisa ser referenciada por seus pais.
Você está violando a ordem das operações do design. Uma API REST destina-se especificamente a ser independente do consumidor. A API não deve ser construída de acordo com as especificações de um de seus consumidores.
Quando você diz "Quero ver os deltas na árvore", o que você realmente está dizendo é "o aplicativo consumidor apenas precisa ver os deltas na árvore". Mas isso não importa muito para a API REST. Apenas fornece uma abordagem padronizada.
É da natureza das abordagens padronizadas muitas vezes carecer de ferramentas altamente personalizáveis e, em vez disso, favorecer as ferramentas mais usadas .
Você pode se desviar do caminho? Bem, funcionará em um nível técnico. Mas não será mais puro descanso. Isso é algo altamente contextual e você precisa avaliar as opções.
fonte
A principal, na minha opinião, é a capacidade de delegar trabalho a componentes genéricos que conhecem apenas os padrões, não o seu caso de negócios específico.
Se você está aderindo à interface uniforme, é mais fácil para outras partes criar componentes que se integram perfeitamente aos seus.
Aqui está Fielding escrevendo em 2008
Uma das maneiras pelas quais gerenciamos a "vida longa" é ter um padrão claro que descreva a semântica das mensagens que estamos passando. Se todos concordarem com o que
PUT
significa, os consumidores e produtores dessas solicitações poderão ser desenvolvidos de forma independente, e os componentes intermediários entre as duas poderão executar ações sensatas sem precisar conhecer os detalhes da mensagem em seu contexto específico.Qual é o sentido de usar
PUT
então?Essa é uma linha de solicitação perfeitamente válida para uma mensagem HTTP, e a mudança na semântica não vai confundir ninguém.
Equivalentemente
Que praticamente tem semântica sem restrições; o servidor pode implementar o tratamento dessa solicitação da maneira que desejar.
Sua relutância em usar o PATCH é especialmente estranha nesse caso, pois já existem padrões propostos para o JSON Patch e o JSON Merge Patch - o trabalho de padronizar um formato de documento de patch já pode ser feito para você.
Outra alternativa válida seria tratar o documento de correção como um recurso separado. Semanticamente, você pode imaginar algo como
Isso faz com que você seja honesto, uniforme, semântica de mensagens, sacrificando a invalidação de cache padronizada .
Em uma configuração de revisão de código, rejeitaria uma alteração proposta que tentasse redefinir a semântica de PUT.
A mesma consideração vale
PUT
também; se sua implementação de PUT se desviar da semântica padronizada, sua implementação será responsável pelo dano resultante.Está perfeitamente bem. O REST não se importa com as grafias usadas para os identificadores de recursos.
Considere a página de destino do Google. Você precisa prestar atenção à ortografia do URI para o doodle de hoje? ou para onde o formulário de pesquisa está sendo enviado? Não, claro que não. A carga útil HTML inclui URI, e os clientes apenas usam os identificadores fornecidos, de maneira padrão, sem a necessidade de analisar esses identificadores.
As informações codificadas em um URI ficam a critério do servidor de origem, para seus próprios fins.
Gostaria de desencorajar o uso de um URI como o ponto de entrada da sua API.
https://www.example.org/df8f5f87-15ff-4212-8fb8-4fbca2c7efcf
é um pouco estranho para o consumo humano. Um URI legível por humanos que redireciona para o recurso UUID seria bom, um URI legível por humanos que retorna o conteúdo do recurso UUID seria melhor.Tudo bem - novamente, olhe para o padrão .
Em alguns casos, faz sentido enviar a nova representação do recurso como parte da resposta (para poupar o cliente da latência de uma solicitação / resposta GET).
fonte
What's the point in using PUT then?
-- De fato. Um POST funcionaria perfeitamente bem.PATCH
? A definição se encaixa melhor.REST é um estilo de arquitetura para manipular recursos de maneira sem estado (onde sem estado significa que cada manipulação se sustenta por si mesma e não depende de outras manipulações que possam ter ocorrido).
O uso dos verbos PUT / PATCH / POST / GET / DELETE vem do uso comum do protocolo HTTP usado para transferir e manipular os recursos. O significado desses verbos é definido por um padrão da Internet ( RFC7231 ).
Dado esse histórico, seu uso do PUT não é padrão e pode confundir outros desenvolvedores que desejam usar sua API.
Com relação aos caminhos dos recursos, quase ninguém se importa com a ortografia exata (incluindo se um recurso filho está listado como filho). O que as pessoas se preocupam é que cada recurso seja identificado exclusivamente. O sistema
/parent/:pid/child/:cid
é frequentemente usado quando os IDs filhos são únicos dentro de um pai para ainda ter um caminho globalmente exclusivo para o recurso.fonte
Parece-me que você realmente deveria estar usando PATCH como semântica aqui.
PUT explica o estado exato desejado. Isso é útil quando um recurso pode estar mudando com freqüência e a alteração desejada precisa ocorrer dentro de um contexto específico.
PATCH explica o delta desejado para o que estiver lá. Isso é útil quando a mudança não se importa com o contexto ou o contexto relevante é muito menor que o recurso inteiro.
Uma imagem é um bom exemplo onde faria sentido fazer o upload da coisa toda. É importante que todo o recurso seja comunicado para garantir um contexto relevante.
A atualização inversa da contagem de reprodução em uma lista de reprodução de música pode fazer mais sentido ser um delta. Não porque é uma pequena alteração, mas porque reenviar a lista inteira pode facilmente desfazer alterações no conteúdo da lista.
Não, você realmente não precisa usar caminhos - nunca. Digamos que você mantenha todos os seus arquivos na área de trabalho? Não? Por que não?
Provavelmente algo a ver com tornar mais fácil ver o mesmo problema aqui. Um GUID não informa o que você está manipulando enquanto está configurando, depurando ou executando.
Então, como é apoiar esse sistema? ou para interagir com ele? Se você ainda não pensou nisso, leve algum tempo e considere o trabalho que está empurrando na linha.
Ter informações explícitas de caminhos serve para ajudar a validar a solicitação. Também ajuda a diferenciar informações o suficiente para que os desenvolvedores de sistemas de suporte e downstream possam abordar esses URLs e usá-los.
Você pode impor um limite de profundidade, simplesmente para que um garoto esperto não obtenha a raiz do seu site para todas as operações que eles possam imaginar.
Se a atualização de um recurso está afetando seu pai de uma maneira não trivial e previsível, você tem outros problemas ... Você realmente precisa examinar o modelo de estado e descobrir por que as informações estão pulando.
Se você deseja retornar apenas uma lista detalhada de deltas, por que não? Por que não suportar várias visualizações de saída alternadas por vários parâmetros? Jenkins retorna suas respostas de API em uma opção de xml ou json e permite especificar vários filtros para extrair a subárvore desejada.
Use você mesmo
Francamente, afaste-se do que você está criando e tente apoiá-lo ou faça outro aplicativo para usá-lo (não um dos aplicativos pré-existentes). Faça algo semelhante para APIs de terceiros para ter um pouco de contexto em segundo plano.
Sempre que você tiver que fazer algo que não está resolvendo diretamente a solicitação de suporte ou diretamente necessário para o aplicativo cliente, a API é menos que o ideal e você sabe por que é menos que o ideal, o que é ainda melhor, porque você pode conserte ou não cometa o mesmo erro.
Por exemplo, se as solicitações para um determinado URL falham constantemente, quanto esforço você tem para investir para determinar o que está falhando e por quê? Que medidas você tomou, poderia ter evitado uma dessas etapas por ter um URI melhor, ou melhor registro, ou melhor monitoramento, ou etc ...?
Da mesma forma, se você estiver escrevendo um novo cliente, quantas vezes você precisa consultar a documentação ou o código-fonte da API? O que você poderia fazer para reduzir essa necessidade? O que você poderia fazer para parar de violar suas próprias expectativas? O que você poderia fazer para simplificar o problema dos aplicativos clientes sem tornar o servidor um pesadelo para manter?
Caminho certo
Francamente, o caminho certo é circunstancial. O REST é um conjunto de práticas demonstradas para funcionar em várias circunstâncias para vários problemas. Se o seu problema não se encaixar, não o faça, mas também não pretenda usar essas práticas.
fonte
A maioria dos recursos das APIs REST existe por um motivo, mas pode não ser relevante para você. Fornecer todo o recurso, como em PUT, por exemplo, é relevante se você precisar de idempotência, caso contrário, não é. (Embora eu ache que seria melhor para os usuários / colegas / o que quer que seja anunciar o fato de que seu terminal não é idempotente usando POST ou PATCH.)
Para a coisa do caminho, nunca ouvi dizer que estava relacionado ao descanso.
/root/345dd4dc-e175-455f-b545-85b1b1ce3e82
faz parte de uma árvore tanto quanto/foo/bar/baz
. Talvez um pouco menos amigável, mas não menos tranquilo, tanto quanto eu posso ver.Se você quiser um raciocínio mais detalhado sobre o motivo pelo qual o REST foi projetado, acho que deveria ler a dissertação original: https://www.ics.uci.edu/~fielding/pubs/dissertation/fielding_dissertation.pdf
Lendo que você pode descobrir que é bem diferente de como o REST é representado em conversas ou usado nas APIs hoje. Claramente, muitas outras pessoas encontraram uma razão, boa ou má, para se afastar dela.
Eu particularmente gosto desta citação que você pode achar relevante:
fonte
PUT
solicitações várias vezes sem que você saiba, e eles podem fazer isso porque a especificação HTTP diz quePUT
é idempotente . Se seusPUT
s não são idempotentes, isso interromperá seu serviço .GET
eHEAD
é pura e sem efeitos colaterais. Algumas pessoas perderam dados, porque estavam usando aplicativos da Web mal projetados, onde a exclusão do conteúdo era feita com umaGET
solicitação e o acelerador da Web enviavaGET
solicitações para todos os URIs que conseguisse encontrar.