JSON, roteamento e aninhamento
Você pode encontrar todo o código para este capítulo aqui
No capítulo anterior nós criamos um servidor web para armazenar quantos jogos nossos jogadores venceram.
Nossa gerente de produtos veio com um novo requisito; criar um novo endpoint chamado /liga
que retorne uma lista contendo todos os jogadores armazenados. Ela gostaria que isto fosse retornado como um JSON.
Este é o código que temos até agora
Você pode encontrar os testes correspondentes no endereço no topo do capítulo.
Nós vamos começar criando o endpoint para a tabela de liga
.
Escreva os testes primeiro
Ampliaremos a suite de testes existente, pois temos algumas funções de teste úteis e um ArmazenamentoJogador
falso para usar.
Antes de nos preocuparmos sobre as pontuações atuais e o JSON, nós vamos tentar manter as mudanças pequenas com o plano de ir passo a passo rumo ao nosso objetivo. O início mais simples é checar se nós conseguimos consultar /liga
e obter um OK
de retorno.
Tente rodar os testes
Seu ServidorJogador
deve estar sendo abortado por um panic como acima. Vá para a linha de código que está apontando para servidor.go
no stack trace.
No capítulo anterior, nós mencionamos que esta era uma maneira bastante ingênua de fazer o nosso roteamento. O que está acontecendo é que ele está tentando cortar a string do caminho da URL começando do índice após /liga
e então, isto nos dá um slice bounds out of range
.
Escreva somente o código suficiente para fazê-lo passar
Go tem um mecanismo de rotas nativo (built-in) chamado ServeMux
(requisição multiplexadora) que nos permite atracar um http.Handler
para caminhos de uma requisição em específico.
Vamos cometer alguns pecados e obter os testes passando da maneira mais rápida que pudermos, sabendo que nós podemos refatorar isto com segurança uma vez que nós soubermos que os testes estão passando.
Quando a requisição começa nós criamos um roteador e então dizemos para o caminho
x
usar o handlery
.Então para nosso novo endpoint, nós usamos
http.HandlerFunc
e uma função anônima paraw.WriteHeader(http.StatusOK)
quando/liga
é requisitada para fazer nosso novo teste passar.Para a rota
/jogadores/
nós somente recortamos e colamos nosso código dentro de outrohttp.HandlerFunc
.Finalmente, nós lidamos com a requisição que está vindo chamando nosso novo roteador
ServeHTTP
(notou comoServeMux
é também umhttp.Handler
?)
Refatorando
ServeHTTP
parece um pouco grande, nós podemos separar as coisas um pouco refatorando nossos handlers em métodos separados.
É um pouco estranho (e ineficiente) estar configurando um roteador quando uma requisição chegar e então chamá-lo. O que idealmente queremos fazer é uma função do tipo NovoServidorJogador
que pegará nossas dependências e ao ser chamada, irá fazer a configuração única da criação do roteador. Desta forma, cada requisição pode usar somente uma instância do nosso roteador.
ServidorJogador
agora precisa armazenar um roteador.Nós movemos a criação do roteador para fora de
ServeHTTP
e colocamos dentro do nossoNovoServidorJogador
, então isto só será feito uma vez, não por requisição.Você vai precisar atualizar todos os testes e código de produção onde nós costumávamos fazer
ServidorJogador{&armazenamento}
porNovoServidorJogador(&armazenamento)
.
Uma refatoração final
Tente mudar o código para o seguinte:
Finalmente, se certifique de que você deletou func (s *ServidorJogador) ServeHTTP(w http.ResponseWriter, r *http.Request)
por não ser mais necessária!
Incorporando
Nós mudamos a segunda propriedade de ServidorJogador
removendo a propriedade nomeada roteador http.ServeMux
e substituindo por http.Handler
; isto é chamado de incorporar.
O Go não provê a noção típica de subclasses orientada por tipo, mas tem a habilidade de "emprestar" partes de uma implementação por incorporar tipos dentro de uma struct ou interface.
O que isto quer dizer é que nosso ServidorJogador
agora tem todos os métodos que http.Handler
têm, que é somente o ServeHTTP
.
Para "preencher" o http.Handler
nós atribuímos ele para o roteador
que nós criamos em NovoServidorJogador
. Nós podemos fazer isso porque http.ServeMux
tem o método ServeHTTP
.
Isto nos permite remover nosso próprio método ServeHTTP
, pois nós já estamos expondo um via o tipo incorporado.
Incorporamento é um recurso muito interessante da linguagem. Você pode usar isto com interfaces para compor novas interfaces.
E você pode usar isto com tipos concretos também, não somente interfaces. Como você pode esperar, se você incorporar um tipo concreto você vai ter acesso a todos os seus métodos e campos públicos.
Alguma desvantagem?
Você deve ter cuidado ao incorporar tipos porque você vai expor todos os métodos e campos públicos do tipo que você incorporou. Em nosso caso, está tudo bem porque nós haviamos incorporado apenas a interface que nós queremos expôr (http.Handler
).
Se nós tivéssemos sido "preguiçosos" e incorporado http.ServeMux
(o tipo concreto) por exemplo, também funcionaria porém os usuários de ServidorJogador
seriam capazes de adicionar novas rotas ao nosso servidor porque o método Handle(path, handler)
seria público.
Quando incorporamos tipos, realmente devemos pensar sobre qual o impacto que isto terá em nossa API pública
Isto é um erro muito comum de mau uso de incorporamento, que termina poluindo nossas APIs e expondo os métodos internos dos seus tipos incorporados.
Agora que nós reestruturamos nossa aplicação, nós podemos facilmente adicionar novas rotas e botar para funcionar nosso endpoint /liga
. Agora precisamos fazê-lo retornar algumas informações úteis.
Nós devemos retornar um JSON semelhante a este:
Escreva o teste primeiro
Nós vamos começar tentando analizar a resposta dentro de algo mais significativo.
Por que não testar o JSON como texto puro?
Você pode argumentar que um simples teste inicial poderia só comparar que o não foi possível ouvir na porta 5000 tem um particular texto em JSON.
Na minha experiência, testes que comparam JSONs de forma literal possuem os seguintes problemas:
Fragilidade. Se você mudar o modelo dos dados seu teste irá falhar.
Difícil de debugar. Pode ser complicado de entender qual é o problema real ao se comparar dois textos JSON.
Má intenção. Embora a saída deva ser JSON, o que é realmente importante é exatamente o que o dado é, ao invés de como ele está codificado.
Re-testando a biblioteca padrão. Não há a necessidade de testar como a biblioteca padrão gera JSON, ela já está testada. Não teste o código de outras pessoas.
Ao invés disso, nós poderíamos analisar o JSON dentro de estruturas de dados que são relevantes para nós e nossos testes.
Modelagem de dados
Dado o modelo de dados do JSON, parece que nós precisamos de uma lista de Jogador
com alguns campos, sendo assim nós criaremos um novo tipo para capturarmos isso.
Decodificação de JSON
Para analizar o JSON dentro de nosso modelo de dados nós criamos um Decoder
do pacote encoding/json
e então chamamos seu método Decode
. Para criar um Decoder
é necessário ler de um io.Reader
, que em nosso caso é nossa própria resposta Body
.
Decode
pega o endereço da coisa que nós estamos tentando decodificar, e é por isso que nós declaramos um slice vazio de Jogador
na linha anterior.
Esse processo de analisar um JSON pode falhar, então Decode
pode retornar um error
. Não há ponto de continuidade para o teste se isto acontecer, então nós checamos o erro e paramos o teste com t.Fatalf
. Note que nós exibimos o não foi possível ouvir na porta 5000 junto do erro, pois é importante para qualquer outra pessoa que esteja rodando os testes ver que o texto não pôde ser analisado.
Tente rodar o teste
Nosso endpoint atualmente não retorna um corpo, então isso não pode ser analisado como JSON.
Escreva código suficiente para fazê-lo passar
Os testes agora passam.
Codificando e decodificando
Note a amável simetria na biblioteca padrão.
Para criar um
Encoder
você precisa de umio.Writer
que é o quehttp.ResponseWriter
implementa.Para criar um
Decoder
você precisa de umio.Reader
que o campoBody
da nossa resposta implementa.
Ao longo deste livro, nós temos usado io.Writer
. Isso é uma outra demonstração desta prevalência nas bibliotecas padrões e de como várias bibliotecas facilmente trabalham em conjunto com elas.
Refatoração
Seria legal introduzir uma separação de conceitos entre nosso handler e o trecho de obter o tabelaDaLiga
. Como sabemos, nós não vamos codificar isso por agora.
Mais adiante, nós vamos querer estender nossos testes para então podermos controlar exatamente qual dado nós queremos receber de volta.
Escreva o teste primeiro
Nós podemos atualizar o teste para afirmar que a tabela das ligas contem alguns jogadores que nós vamos pôr em nossa loja.
Atualize EsbocoArmazenamentoJogador
para permitir que ele armazene uma liga, que é apenas um slice de Jogador
. Nós vamos armazenar nossos dados esperados lá.
Adiante, atualize nossos testes colocando alguns jogadores na propriedade da liga, para então afirmar que eles foram retornados do nosso servidor.
Tente rodar o teste
Escreva o minimo de código para que o teste rode e cheque as falhas na saída dele.
Você vai precisar atualizar os outros testes, assim como nós temos um novo campo em EsbocoArmazenamentoJogador
; ponha-o como nulo para os outros testes.
Tente executar os testes novamente e você deverá ter:
Escreva código suficiente para fazê-lo passar
Nós sabemos que o dado está em nosso EsbocoArmazenamentoJogador
e nós abstraímos esses dados para uma interface ArmazenamentoJogador
. Nós precisamos atualizar isto então qualquer um passando-nos um ArmazenamentoJogador
pode prover-nos com dados para as ligas.
Agora nós podemos atualizar o código do nosso handler para chamar isto ao invés de retornar uma lista manualmente escrita. Delete nosso método obterTabelaDaLiga()
e então atualize manipulaLiga
para chamar ObterLiga()
.
Tente executar os testes:
O compilador está reclamando porque ArmazenamentoDeJogadorNaMemoria
e EsbocoArmazenamentoJogador
não tem os novos métodos que nós adicionamos em nossa interface.
Para EsbocoArmazenamentoJogador
isto é bem fácil, apenas retorne o campo liga
que nós adicionamos anteriormente.
Aqui está uma lembrança de como InMemoryStore
é implementado:
Embora seja bastante simples para implementar ObterLiga
"propriamente", iterando sobre o map, lembre que nós estamos apenas tentando escrever o mínimo de código para fazer os testes passarem.
Então vamos apenas deixar o compilador feliz por enquanto e viver com o desconfortável sentimento de uma implementação incompleta em nosso InMemoryStore
.
O que isto está realmente nos dizendo é que depois nós vamos querer testar isto, porém vamos estacionar isto por hora.
Tente executar os testes, o compilador deve passar e os testes deverão estar passando!
Refatoração
O código de teste não transmite suas intenções muito bem e possui vários trechos que podem ser refatorados.
Aqui estão os novos helpers:
Uma última coisa que nós precisamos fazer para nosso servidor funcionar é ter certeza de que nós retornamos um content-type
correto na resposta, então as máquinas podem reconhecer que nós estamos retornando um JSON
.
Escreva os testes primeiro
Adicione essa afirmação no teste existente
Tente rodar o teste
Escreva código suficiente para fazê-lo passar
Atualize manipulaLiga
O teste deve passar.
Refatoração
Adicione um helper para verificaTipoDoConteudo
.
Use isso no teste.
Agora que nós resolvemos ServidorJogador
, por agora podemos mudar nossa atenção para ArmazenamentoDeJogadorNaMemoria
porque no momento se nós tentarmos demonstrá-lo para o gerente de produto, /liga
não vai funcionar.
A forma mais rápida de nós termos alguma confiança é adicionar a nosso teste de integração, nós podemos bater no novo endpoint e checar se nós recebemos a resposta correta de /liga
.
Escreva o teste primeiro
Nós podemos usar t.Run
para parar este teste um pouco e então reusar os helpers dos testes do nosso servidor - novamente mostrando a importância de refatoração dos testes.
Tente rodar o teste
Escreva código suficiente para fazê-lo passar
ArmazenamentoDeJogadorNaMemoria
is returning nil
when you call ObterLiga()
so we'll need to fix that.
Tudo que nós precisamos fazer é iterar através do map e converter cada chave/valor para um Jogador
O teste deve passar agora.
Concluindo
Nós temos continuado a seguramente iterar no nosso programa usando TDD, fazendo ele suportar novos endpoints de uma forma manutenível com um roteador e isso pode agora retornar JSON para nossos consumidores. No próximo capítulo, nós vamos cobrir persistência de dados e ordenação de nossas ligas.
O que nós cobrimos:
Roteamento. A biblioteca padrão oferece uma fácil forma de usar tipos para fazer roteamento. Ela abraça completamente a interface
http.Handler
nela, tanto que você pode atribuir rotas paraHandler
s e a rota em si também é umHandler
. Ela não tem alguns recursos que você pode esperar, como caminhos para variáveis (ex./users/{id}
). Você pode facilmente analisar esta informação por si mesmo porém você pode querer considerar olhar para outras bibliotecas de roteamento se isso se tornar um fardo. Muitas das mais populares seguem a filosofia das bibliotecas padrões e também implementamhttp.Handler
.Composição. Nós tocamos um pouco nesta técnica porém você pode ler mais sobre isso de Effective Go. Se há uma coisa que você deve tirar disso é que composições podem ser extremamente úteis, porém sempre pensando na sua API pública, só exponha o que é apropriado.
Serialização e Desserialização de JSON. A biblioteca padrão faz isto de forma bastante trivial ao serializar e desserializar nosso dado. Isto também abre para configurações e você pode customizar como esta transformação de dados funciona se necessário.
Last updated