Reflexão
Last updated
Was this helpful?
Last updated
Was this helpful?
Desafio Golang: escreva uma função
percorre(x interface{}, fn func(string))
que recebe uma structx
e chamafn
para todos os campos string encontrados dentro dela. nível de dificuldade: recursão.
Para fazer isso vamos precisar usar reflection
(reflexão).
A reflexão em computação é a habilidade de um programa examinar sua própria estrutura, particularmente através de tipos; é uma forma de metaprogramação. Também é uma ótima fonte de confusão.
De
interface
?Aproveitamos a segurança de tipos que o Go nos ofereceu em termos de funções que funcionam com tipos conhecidos, como string
, int
e nossos próprios tipos como ContaBancaria
.
Isso significa que de praxe temos documentação e o compilador vai reclamar se você tentar passar o tipo errado para uma função.
Só que você pode se deparar com situações em que quer escrever uma função, mas não sabe o tipo da variável em tempo de compilação.
Go nos permite contornar isso com o tipo interface{}
, que você pode relacionar com qualquer tipo.
Logo, percorre(x interface{}, fn func(string))
aceitará qualquer valor para x
.
interface
para tudo e ter funções bem flexíveis?Quando utiliza uma função que usa interface
, você perde a segurança de tipos. E se você quisesse passar Foo.bar
do tipo string
para uma função, mas ao invés disso passa Foo.baz
do tipo int
? O compilador não vai ser capaz de informar seu erro. Você também não tem ideia do que pode passar para uma função. Saber que uma função recebe um ServicoDeUsuario
, por exemplo, é muito útil.
Resumindo, só use reflexão quando realmente precisar.
Se quiser funções polimórficas, considere desenvolvê-la em torno de uma interface (não interface{}
, só para esclarecer) para que os usuários possam usar sua função com vários tipos se implementarem os métodos que você precisar para a sua função funcionar.
Nossa função vai precisar ser capaz de trabalhar com várias coisas diferentes. Como sempre, vamos usar uma abordagem iterativa, escrevendo testes para cada coisa nova que quisermos dar suporte e refatorando ao longo do caminho até finalizarmos.
Vamos chamar nossa função com uma estrutura que tem um campo string dentro (x
). Depois, podemos espiar a função (fn
) passada para ela para ver se ela foi chamada.
Queremos armazenar um slice de strings (resultado
) que armazena quais strings foram passadas dentro de fn
pelo percorre
. Algumas vezes, nos capítulos anteriores, criamos tipos dedicados para isso para espionar chamadas de função/método, mas nesse caso vamos apenas passá-lo em uma função anônima para fn
que acaba em resultado
.
Usamos uma struct
anônima com um campo Nome
do tipo string para partir para caminho "feliz" e mais simples.
Finalmente, chamamos percorre
com x
e o espião e por enquanto só verificamos o tamanho de resultado
. Teremos mais precisão nas nossas verificações quando tivermos algo bem básico funcionando.
Precisamos definir percorre
.
Execute o teste novamente:
Agora podemos chamar o espião com qualquer string para fazer o teste passar.
Agora o teste deve estar passando. A próxima coisa que vamos precisar fazer é criar uma verificação mais específica do que está sendo chamado dentro do nosso fn
.
Adicione o código a seguir para o teste existente para verificar se a string passada para fn
está correta:
Esse código está pouco seguro e muito frágil, mas lembre-se que nosso objetivo quando estamos no "vermelho" (os testes estão falhando) é escrever a menor quantidade de código possível. Depois escrevemos mais testes para resolver nossas lacunas.
Precisamos usar o reflection para verificar as propriedades de x
.
Então podemos presumir coisas bem otimistas sobre o valor passado:
Podemos procurar pelo primeiro e único campo, mas pode não haver nenhum campo, o que causaria um pânico.
Depois podemos chamar String()
que tetorna o valor subjacente como string, mas sabemos que vai dar errado se o campo for de algum tipo que não uma string.
Nosso código está passando pelo caso simples, mas sabemos que nosso código tem várias falhas.
Vamos escrever alguns testes onde passamos valores diferentes e verificaremos o array de strings com que fn
foi chamado.
Precisamos refatorar nosso teste em um teste orientado por tabelas para tornar esse processo mais fácil para continuarmos testando novas situações.
Agora podemos adicionar uma situação facilmente para ver o que acontece se tivermos mais de um campo string.
Adicione o cenário a seguir nos casos
.
valor
tem um método chamado NumField
que retorna a quantidade de campos no valor. Isso nos permite iterar sobre os campos e chamar fn
, o que faz nosso teste passar.
Não parece haver nenhuma refatoração óbvia aqui que pode melhorar nosso código, então vamos continuar.
A próxima falha em percorre
é que ela presume que todo campo é uma string
. Vamos escrever um teste para esse caso.
Inclua o seguinte cenário:
Precisamos verificar que o tipo do campo é uma string
.
Parece que o código ainda está razoável por enquanto.
O próximo caso é: e se o valor não for uma struct
"única"? Em outras palavras, o que acontece se tivermos uma struct
com alguns campos aninhados?
Estivemos usando a sintaxe de estrutura anônima para declarar tipos conforme precisávamos para nossos testes, então poderíamos continuar a fazer isso, como:
Vamos apenas refatorar isso criando um tipo conhecido para esse caso e referenciá-lo no nosso teste. Não é aconselhável colocar código do teste fora do teste, mas as pessoas devem ser capazes de encontrar essas estruturas procurando por sua definição.
Inclua as seguintes declarações de tipos no seu arquivo de teste:
Agora podemos adicionar isso aos nossos casos ficarem bem mais legíveis que antes:
O problema é que estamos apenas iterando sobre os campos no primeiro nível da hierarquia de tipos.
A solução é bem simples. Inspecionamos seu tipo novamente e se for uma estrutura apenas chamamos percorre
novamente na nossa estrutura de dentro.
Quando você está fazendo uma comparação de mesmo valor mais de uma vez, geralmente refatorar as condições dentro de um switch
vai melhorar a legibilidade e tornar seu código mais fácil de estender.
E se o valor passado na estrutura for um ponteiro?
Inclua esse caso:
Não é possível usar o NumField
em um ponteiro Value
e precisamos extrair o valor antes disso usando Elem()
.
Vamos encapsular a responsabilidade de extrair o reflect.Value
de determinada interface{}
para uma função.
Isso acaba adicionando mais código, mas me parece que o nível de abstração está correto.
Obter o reflect.Value
de x
para que eu possa inspecioná-lo, não me importa de qual forma.
Iterar pelos campos, fazendo o que for necessário dependendo de seu tipo.
Depois precisamos lidar com os slices.
Esse caso se parece bastante com o do ponteiro acima, pois estamos chamar NumField
em nosso reflect.Value
, mas não há um por não ser uma struct.
Isso funciona, mas está bagunçado. Não se preocupe, pois temos cada pedaço de código coberto por testes e podemos brincar da forma que quisermos.
Se formos pensar um pouco abstradamente, queremos chamar percorre
em:
Cada campo de uma estrutura
Cada coisa de um slice
No momento nosso código faz isso, mas não reflete muito bem. Precisamos ter uma verificação no início da função para certificar se é um slice (com um return
para parar a execução do restante do código) e se não for, só vamos presumir que é uma estrutura.
Vamos retrabalhar o código para verificar o tipo primeiro para depois fazermos o que importa.
Parece muito melhor! Se for uma estrutura ou um slice, iteramos sobre seus valores chamando percorre
para cada um. Por outro lado, se for um reflect.String
, podemos apenas chamar fn
.
Ainda assim me parece que poderia ficar melhor. Há repetição da operação de iterar sobre campos/valores e chamar percorre
sendo que conceitualmente são a mesma coisa.
Se o valor
for um reflect.String
, chamamos fn
normalmente.
Se for outra coisa, nosso switch
vai extrair duas coisas dependendo do tipo:
Quantos campos existem
Como extrair o Value
(Field
[campo] ou Index
[índice])
Uma vez que determinamos esses pontos, podemos iterar pela quantidadeDeValores
chamando percorre
com o resultado da função getField
.
A partir disso, lidar com arrays deve ser simples.
Inclua o caso:
Podemos resolver o caso dos arrays da mesma forma que os slices, basta adicioná-los com uma vírgula:
O último tipo que queremos lidar é o map
.
Novamente, se pensar um pouco de forma abstrata, percebe-se que o map
é bem parecido com a struct
, mas as chaves são desconhecidas em tempo de compilação.
Again if you think a little abstractly you can see that map
is very similar to struct
, it's just the keys are unknown at compile time.
No entanto, por design, não é possível obter os valores de um map por índice. Só é possível fazer isso pela chave, que, caramba, acaba com a nossa abstração.
Como se sente agora? Parecia que essa era uma boa abstração naquele momento, mas agora o código parece um pouco bagunçado.
Está tudo bem! Refatoração é uma jornada e às vezes vamos cometer erros. Um ponto importante do TDD é que ele nos dá a liberdade de testar esse tipo de coisa.
Graças aos testes implmentados a cada etapa, essa situação não é irreversível de forma alguma. Vamos apenas voltar a como estava antes da refatoração.
Apresentamos o percorreValor
, que encapsula chamadas para percorre
dentro do nosso switch
para que só tenham que extrair os reflect.Value
de valor
.
Lembre que maps em Go não têm ordem garantida. Logo, às vezes os testes irão falhar porque verificamos as chamadas de fn
em uma ordem específica.
Para arrumar isso, precisaremos mover nossa verificação com os maps para um novo teste onde não nos importamos com a ordem.
Essa é a definição de verificaSeContem
:
Apresentamos alguns dos conceitos do pacote reflect
.
Usamos recursão para percorrer estruturas de dados arbitrárias.
Houve uma reflexão quanto a uma refatoração ruim, mas não há por que se preocupar muito com isso. Isso não deve ser um problema muito grande se trabalharmos com testes de forma iterativa.
Agora que você tem conhecimento sobre reflexão, faça o possível para evitá-lo.
No existe uma função chamada ValueOf
que retorna um Value
(valor) de determinada variável. Isso nos permite inspecionar um valor, inclusive seus campos usados nas próximas linhas.
Podemos verificar seu tipo chamando a função .
Mas podemos ver que quando você usa estruturas anônimas cada vez mais aninhadas, a sintaxe fica um pouco bagunçada. .
Esse capítulo só cobre um aspecto pequeno de reflexão. .