Entrando no mundo da Metaprogramação

O que é Metaprogramação?

Resumindo bem, a metaprogramação nada mais é do que “código que gera código”. Ou seja, é usufruir de recursos que algumas linguagens de programação oferecem para gerar códigos automaticamente. E espeficamente para o Ruby, alguns deles em tempo de execução.

 

O que vamos aprender?

  • O que é metaprogramação
  • Os métodos que nos apoiam para isso
  • Mostrar exemplo para aplicar os conceitos \o/

 

O que vamos construir?

Neste post nós vamos simular uma empresa de entregas. Para isso, criaremos duas classes, uma para os produtos (Product) e outra para os pacotes que serão entregues (Package).

Primeiro vamos construir estas classes de maneira tradicional e depois aplicar os conceitos da metaprogramação para nos ajudar a deixá-las mais avançadas : )

 

Como o Ruby invoca os métodos

Antes de definitamente trabalhar com a metaprogramação, precisamos entender como o Ruby trabalha.

Como já é bem dito, o Ruby é quase 100% orientado a objetos. Ou seja, as comunicações dele funcionam por meio de troca de mensagens e isso ocorre por meio de chamadas de métodos.

Quando instanciamos um objeto e chamamos um método, o Ruby faz a busca primeiro na própria classe do objeto, caso nao encontre procura em algum módulo que esteja sendo importando e caso não encontre procura nas superclasses.

Mas e se nada for encontrado? Bem, esta é aqui que reside parte do segredo e que devemos tomar muito cuidado para não abrir uma caixa de pandora. Se nada for encontrado, o Ruby vai fazer a chamada de um método chamado method_missing que está presente na classe BasicObject, uma classe a qual todo objeto Ruby herda. E, adivinhe! Nós podemos sobrescrever este método na nossa classe e definir um método dinamicamente.  : )

 

Métodos que ajudam na metaprogramação

Existem métodos que nos ajudam na hora de trabalhar com a metaprogramação. Seja no momento de chamada do method_missing ou na definição dinânima da própria classe. Eles são:

  • respond_to? – Este método é utilizado para ver se o objeto tem um determinado método definido
  • method_missing – É chamado quando nenhum método é encontrado
  • define_method ou define_singleton_method – Com este método nós conseguimos criar outros métodos dinamicamente. A diferença é que o define_method é usado no contexto da classe e o define_singleton_method no contexto do objeto.
  • respond_to_missing? – Este método é chamado quando o respond_to? não encontra o métodos que você busca. Mas vamos entender melhor no código.
  • instance_variable_set – Usando este método, é possível atribuir um valor a uma variável de instância dinamicamente.
  • instance_variable_get – Usando este método, é possível pegar o valor de uma variável de instância dinamicamente.
  • send – Com este método, é possivel invocar o método desejado passando o nome como string ou symbol.

 

Então, bora codar!

Primeiro nós vamos criar duas classes básicas, nada de especial. Depois, vamos usar os métodos que vimos lá em cima pra nos divertir um pouco. o/

 

1. Primeiro, vamos criar um diretório chamado delivery

 

2. Vamos entrar no diretório

 

3. Criaremos o arquivo das duas classes que vamos utilizar:

 

4. No arquivo product, colocaremos o seguinte conteúdo (product.rb):

 

5. No arquivo package, colocaremos (package.rb):

 

Neste codigo inicial, temos a classe Product que possui como variáveis de instância name, type e price e possui também métodos de acesso a estas variáveis por meio do attr_accessor.

Há também a classe Package, que prossui uma lista de produtos. Esta lista é incrementada por meio do método add_product que recebe o produto e a quantidade e armazena num hash que será inserido na lista de produtos com as chaves info, que terá o objeto produto e quantity com a quantidade de itens daquele produto. Além disso, temos também dois métodos, um para adicionar etiquetas ao pacote (add_tags) e outro para adicionar os selos (add_stamps).

Terminada esta parte inicial, vamos incrementar o uso da metaprogramação para aperfeiçoar nossas classes.

 

Evoluindo a classe de Produtos

E se resolvêssemos tratar diferentes tipos de produtos com diferentes atributos? Por exemplo, se o produto for uma roupa, é interessante que contenha o tamanho, se for um equipamento eletrônico é legal que seja informado o fabricante.

Para que os novos atributos de cada produto não se misturem com sesus atributos padrões, todo novo atributo terá o valor atribuído através de um método contendo o prefixo “set_” e resgatado através de um método com prefixo “get_”. Por exemplo, os métodos para atribuirmos e resgatarmos o tamanho (“size”) de um produto que seja um camisa serão set_size e get_size.

Para isso, nós vamos utilizar alguns recursos da metaprogramação e melhoras as nossas classes.

1. Adicionaremos o seguinte codigo como privado na classe product (product.rb):

Perceba que definimos aqui o method_missing, que deve ser declarado com dois parâmetros, o nome do método que não foi encontrado (get_size) e os atributos que foram enviados (*args). Por exemplo, se chamamos set_size(“M”), o method_missing será chamado dessa forma: method_missing(:set_size, “M”).

Neste method_missing especificamente, estamos verificando se o método chamado possui o prefixo “get_” ou “set_”. Se possuir o prefixo “set_”, este prefixo é removido (linha 3) e o resto do nome é usado para atribuir um valor numa variável de instancia com instance_variable_set (linha 4). O mesmo ocorre com os métodos que possuem o prefixo “get_”, mas em vez de atribuir, o valor é resgatado da variável de instancia com instance_variable_get (linha 7).

Então, para o método set_size(“M”), o valor “M” é atribuído numa variável de instância chamada size. E para o método get_size, o valor é resgatado desta mesma variável de instância size.

 

2. Adicionaremos também o seguinte método como privado na classe product (product.rb):

Este método respond_to_missing? é chamado quando o método não é encontrado com o respond_to?. Por exemplo, se chamarmos respond_to?(:set_size) em algum objeto do tipo Product sem o método acima ele retornaria falso porque nenhum método com este nome foi formalmente definido.

O método o respond_to? recorre, numa última tentativa, ao respond_to_missing?, e é nele que podemos fazer nossa sobrescrita para que toda vez que o respond_to? for chamado com algum método que tenha prefixo “get_” ou “set_”, retornar verdadeiro.

 

3. Após adicionar estes métodos, teremos a classe product da seguinte forma:

 

Repare que em ambos métodos method_missing e respond_to_missing? nós colocamos o super como última chamada. Fizemos isso para não atrapalharmos o curso natural de busca do Ruby quando os nossos requisitos não forem atendidos.

Por exemplo, se tentarmos chamar um método que não tenha os prefixos que definimos, o curso natural do Ruby é disparar um erro por não encontrar o método e isso só ocorre se houver o super. Isso ocorre porque o super chama do mesmo método corrente mas dessa vez na superclasse que o contém. Estamos praticamente dizendo: “Ruby, o meu método não captou nada que eu precise. Use o seu.”.

 

Definindo métodos dinamicamente

Na nossa classe Package, há dois métodos que são bem parecidos: o add_tag e o add_stamp. Tirando o fato de usarem variáveis de instância diferentes, apenas o que muda de um pro outro são as strings “tag” e “stamp”.

Bem, vamos aproveitar isso e deixar a nossa classe Package um pouco mais desafiadora.

1. Vamos remover a definição dos métodos add_tag e add_stamp da classe Package, que é o código abaixo (package.rb):

 

2. No lugar destes dois métodos, vamos adicionar o seguinte trecho de código:

 

3. Então, a nossa classe Package ficou da deste jeito:

 

O Ruby é incrível. Conforme reparamos anteriormente, as funções add_tag e add_stamp eram idênticas, apenas diferenciando as palavras “tag” e “stamp”. Pois bem, colocamos estas duas palavras numa lista (Array) e iteramos sobre ela. E para cada item da lista, criamos um método como define_method cujo nome foi construído com “add_” + item da lista. Ou seja, estamos criando o mesmos dois métodos add_tag e add_stamp.

O define_method precisa de um parâmetro para o nome do método e um bloco, que será o corpo do método. Os parâmetros do bloco, quando o método for definido, serão os parâmetros do método.

Dentro do define_method, seguimos dois passos. Primeiro resgatamos a variável de instância competente ao método na linha 19. O self.send está chamando o método definido no attr_reader no objeto package atual. Então, caso o método seja add_tag, será invocado o método tags e retornada a variável @tags e caso o método seja add_stamp, será chamado o método stamps e retornanda a variável @stamps.

Recuperada a variável de instância,  na linha 20 utilizamos o send novamente, desta vez na variável de instância que é um Array, para chamar o método << (mesmo que o push) passando o valor que foi passado como argumento do método.

Resumindo a ópera, seria como ter o código transcrito abaixo:

 

Então, galera, está pronto o nosso mini-app de Delivery utilizando recursos de metaprogramação.

 

Agora vamos fazer um teste seguindo a receita:

1- Vamos abrir o console no diretório das classes (delivery):

 

2- Agora, vamos importar as classes

 

3- Criaremos um produto chamado “Camisa Social”, do tipo “Roupa” e preço “80.0”. Depois, armazenaremos o tamanho da camisa com a função “set_size” passando o tamanho “4” como argumento.

 

4- Crieremos um produto chamado “Caneca”, do tipo “Porcelana” e preço “25.00”. Depois, armazenaremos a cor da caneca com a função “set_color” passando a cor “Verde” como argumento.

 

5- Vamos imprimir o conteúdo das duas variáveis de produto, e o resultado será o seguinte:

Perceba que as variáveis @size no product1 e a @color no product2 foram criadas por meio da chamada aos métodos set_size e set_color. Se utilizarmos o get_size e o get_color são exatamente os valores destas variáveis de instância que serão retornados.

 

6- Agora criaremos um pacote e adicione estes dois produtos com a função “add_product”, sendo 3 camisas sociais e 4 canecas.

 

7- Neste pacote criado, adicione 2 etiquetas com o método “add_tag”, uma com “Frágil” e outra com “Avião”, e um selo com o método “add_stamp”, com o valor “Brasil”.

 

8- O último passo é imprimir as variáveis e observar que os resultados foram os esperados:

Os resultados foram os esperados porque ao chamarmos package.tags será retornada a lista com as duas tags “Frágil” e “Avião” que adicionamos, ao chamarmos package.stamps será retornada a lista com o único selo “Brasil” que adicionamos e ao chamarmos package.products será retornado o hash os dois produtos que a quantidade de cada um.



12 formas de vencer o bloqueio criativo e escrever textos memoráveis (e 6 dicas extras)

Não perca nenhum conteúdo

Receba nosso resumo semanal com os novos posts, cursos, talks e vagas \o/



Conclusão

O Ruby tem na metaprogramação uma ferramenta muitíssimo poderosa e por isso devemos tomar um cuidado muito grande com o caminho que estamos seguindo na nossa solução.

A metaprogramação ajuda bastante a começar a imaginar como funcionam as bases de grandes frameworks Ruby e como melhorá-lo.

Pra quem quiser, tem um livro muito bom que vai mais fundo no assunto que é o “Metaprogramming Ruby 2” do autor Paollo Perrota.

É isso, galera. Eu sei que não é um conteúdo fácil para aprender, mas devagar a gente vai absorvendo. Qualquer dúvida, estamos aqui!

 

Um forte abraço!


Você é novo por aqui?

Primeira vez no OneBitCode? Curtiu esse conteúdo? O OneBitCode tem muito mais para você!

O OneBitCode traz conteúdos de qualidade e em português sobre programação com foco em Ruby on Rails e outras tecnologias como Angular, Ionic, React, desenvolvimento de Chatbots e etc.

Se você deseja aprender mais, de uma forma natural e dentro de uma comunidade ativa, visite nosso Facebook e nosso Twitter, veja os screencasts e talks no Youtube, alguns acontecimentos no Instagram, ouça os Podcasts e faça parte de nossa Newsletter.

Além disso, também estamos com alguns e-Books muito interessantes para quem deseja se aprimorar como programador e também como freelancer (os e-Books são gratuitos!):

Espero que curta nossos conteúdos e sempre que precisar de ajuda com os tutoriais, fala com a gente! Seja por Facebook ou e-mail, estamos aqui para você 🙂

Bem-vindo à família OneBitCode \o/

 

Abril 3, 2018

Deixe um comentário

3 Comentários em "Entrando no mundo da Metaprogramação"

avatar
  Subscribe  
newest oldest most voted
Notify of
Luciano Cesar
Visitante

Ótimo artigo, estou aprendendo muito com ele. Valeu!

Kadu Simoes
Visitante

Excelente post Daniel.. Obrigado por compartilhar o conhecimento.. You’ve opened my mind ^^

Feito com s2 por OneBitCode
%d blogueiros gostam disto: