Tenho lido sobre estratégias de controle de versão para APIs ReST, e algo que nenhum deles parece abordar é como você gerencia a base de código subjacente.
Digamos que estejamos fazendo várias alterações significativas em uma API - por exemplo, alterando nosso recurso Customer para que ele retorne campos forename
e separados surname
em vez de um único name
campo. (Para este exemplo, usarei a solução de controle de versão de URL, pois é fácil entender os conceitos envolvidos, mas a questão é igualmente aplicável à negociação de conteúdo ou cabeçalhos HTTP personalizados)
Agora temos um endpoint em http://api.mycompany.com/v1/customers/{id}
e outro endpoint incompatível em http://api.mycompany.com/v2/customers/{id}
. Ainda estamos lançando correções de bugs e atualizações de segurança para a API v1, mas o desenvolvimento de novos recursos agora está focado na v2. Como escrevemos, testamos e implementamos mudanças em nosso servidor API? Posso ver pelo menos duas soluções:
Use um branch / tag de controle de origem para a base de código v1. v1 e v2 são desenvolvidos e implantados de forma independente, com mesclagens de controle de revisão usadas conforme necessário para aplicar a mesma correção de bug a ambas as versões - semelhante a como você gerencia bases de código para aplicativos nativos ao desenvolver uma nova versão principal, embora ainda suporte a versão anterior.
Torne a própria base de código ciente das versões da API, para que você termine com uma única base de código que inclui a representação do cliente v1 e a representação do cliente v2. Trate o controle de versão como parte da arquitetura da sua solução, em vez de um problema de implantação - provavelmente usando alguma combinação de namespaces e roteamento para garantir que as solicitações sejam tratadas pela versão correta.
A vantagem óbvia do modelo de branch é que é trivial excluir versões antigas da API - apenas pare de implantar o branch / tag apropriado - mas se você estiver executando várias versões, pode acabar com uma estrutura de branch e pipeline de implantação realmente complicada. O modelo de "base de código unificada" evita esse problema, mas (eu acho?) Tornaria muito mais difícil remover recursos obsoletos e pontos de extremidade da base de código quando eles não fossem mais necessários. Sei que isso é provavelmente subjetivo, pois é improvável que haja uma resposta correta simples, mas estou curioso para entender como as organizações que mantêm APIs complexas em várias versões estão resolvendo esse problema.
fonte
Respostas:
Eu usei ambas as estratégias que você mencionou. Dessas duas, sou a favor da segunda abordagem, por ser mais simples, nos casos de uso que a suportam. Ou seja, se as necessidades de controle de versão forem simples, use um design de software mais simples:
Não achei muito difícil remover versões obsoletas usando este modelo:
A primeira abordagem é certamente mais simples do ponto de vista de reduzir o conflito entre versões coexistentes, mas a sobrecarga de manter sistemas separados tende a superar o benefício de reduzir o conflito de versões. Dito isso, foi muito simples criar uma nova pilha de API pública e começar a iterar em um branch de API separado. Claro, a perda geracional se instalou quase imediatamente e os ramos se transformaram em uma confusão de fusões, soluções de conflitos de fusões e outras coisas divertidas.
Uma terceira abordagem está na camada de arquitetura: adote uma variante do padrão Facade e abstraia suas APIs em camadas voltadas para o público, com versão que se comunica com a instância de Facade apropriada, que por sua vez se comunica com o back-end por meio de seu próprio conjunto de APIs. Your Façade (usei um adaptador em meu projeto anterior) torna-se seu próprio pacote, independente e testável, e permite que você migre APIs de front-end independentemente do back-end e entre si.
Isso funcionará se suas versões de API tenderem a expor os mesmos tipos de recursos, mas com representações estruturais diferentes, como em seu exemplo de nome completo / nome / sobrenome. Fica um pouco mais difícil se eles começarem a contar com cálculos de back-end diferentes, como em "Meu serviço de back-end retornou juros compostos calculados incorretamente que foram expostos na API v1 pública. Nossos clientes já corrigiram esse comportamento incorreto. Portanto, não posso atualizar isso computação no back-end e aplicá-la até a v2. Portanto, agora precisamos bifurcar nosso código de cálculo de juros. " Felizmente, eles tendem a ser raros: praticamente falando, os consumidores de APIs RESTful favorecem representações precisas de recursos em vez de compatibilidade reversa bug-a-bug, mesmo entre mudanças ininterruptas em um
GET
recurso teoricamente idempotente .Terei interesse em ouvir sua decisão final.
fonte
Para mim, a segunda abordagem é melhor. Eu o usei para os serviços da web SOAP e planejo usá-lo para REST também.
Conforme você escreve, a base de código deve reconhecer a versão, mas uma camada de compatibilidade pode ser usada como camada separada. Em seu exemplo, a base de código pode produzir representação de recurso (JSON ou XML) com nome e sobrenome, mas a camada de compatibilidade irá alterá-lo para ter apenas um nome.
A base de código deve implementar apenas a versão mais recente, digamos v3. A camada de compatibilidade deve converter as solicitações e respostas entre a versão mais recente v3 e as versões suportadas, por exemplo, v1 e v2. A camada de compatibilidade pode ter adaptadores separados para cada versão suportada, que podem ser conectados em cadeia.
Por exemplo:
Solicitação de cliente v1: v1 adapta-se a v2 ---> v2 adapta-se a v3 ----> base de código
Solicitação v2 do cliente: v1 se adapte a v2 (pular) ---> v2 se adapte a v3 ----> base de código
Para a resposta, os adaptadores funcionam simplesmente na direção oposta. Se estiver usando Java EE, você pode usar a cadeia de filtros de servlet como cadeia de adaptadores, por exemplo.
Remover uma versão é fácil, exclua o adaptador correspondente e o código de teste.
fonte
A ramificação parece muito melhor para mim e usei essa abordagem no meu caso.
Sim, como você já mencionou - correções de bugs de backport exigirá algum esforço, mas ao mesmo tempo, o suporte a várias versões em uma base de origem (com roteamento e todas as outras coisas) exigirá se não menos, mas pelo menos o mesmo esforço, tornando o sistema mais complicado e monstruoso com diferentes ramos de lógica dentro (em algum ponto de versionamento você definitivamente chegará a
case()
apontar para módulos de versão com código duplicado, ou pior aindaif(version == 2) then...
). Também não se esqueça de que, para fins de regressão, você ainda precisa manter os testes ramificados.Com relação à política de versionamento: eu manteria no máximo -2 versões da atual, descontinuando o suporte para as antigas - isso daria alguma motivação para os usuários mudarem.
fonte
[Version(From="v1", To="v2")]
,[Version(From="v2", To="v3")]
,[Version(From="v1")] // All versions
apenas explorando-lo agora, já ouviu alguém fazê-lo?Normalmente, a introdução de uma versão principal da API levando você a uma situação de ter que manter várias versões é um evento que não ocorre (ou não deveria ocorrer) com muita frequência. No entanto, isso não pode ser evitado completamente. Eu acho que é geralmente uma suposição segura que uma versão principal, uma vez introduzida, permaneceria como a versão mais recente por um período de tempo relativamente longo. Com base nisso, eu preferiria obter simplicidade no código em detrimento da duplicação, pois isso me dá mais confiança para não quebrar a versão anterior quando introduzo alterações na última.
fonte