Ouvimos frequentemente que a metaprogramação é algo que só os ninjas Ruby usam, e que simplesmente não é para os mortais comuns. Mas a verdade é que a meta-programação não é algo assustador de todo. Este post no blog vai servir para desafiar este tipo de pensamento e para aproximar a metaprogramação do desenvolvedor Ruby médio para que eles também possam colher seus benefícios.
De notar que a metaprogramação pode significar muito e pode muitas vezes ser muito mal utilizada e ir ao extremo quando se trata de utilização, por isso vou tentar dar alguns exemplos do mundo real que todos poderiam usar na programação diária.
Metaprogramming
Metaprogramming é uma técnica pela qual se pode escrever código por si só dinamicamente em tempo de execução. Isto significa que você pode definir métodos e classes durante o tempo de execução. Louco, certo? Em resumo, usando a metaprogramação você pode reabrir e modificar classes, pegar métodos que não existem e criá-los na mosca, criar código que é DRY evitando repetições, e mais.
The Basics
Antes de mergulharmos em metaprogramação séria devemos explorar o básico. E a melhor maneira de fazer isso é através do exemplo. Vamos começar com um e entender a metaprogramação de Ruby passo a passo. Você pode provavelmente adivinhar o que este código está fazendo:
class Developer def self.backend "I am backend developer" end def frontend "I am frontend developer" endend
Definimos uma classe com dois métodos. O primeiro método nesta classe é um método de classe e o segundo é um método de instância. Isto é material básico em Ruby, mas há muito mais a acontecer por detrás deste código que precisamos de compreender antes de prosseguirmos. Vale a pena salientar que a classe Developer
em si mesma é realmente um objecto. Em Ruby tudo é um objecto, incluindo as classes. Uma vez que Developer
é uma instância, é uma instância da classe Class
. Here is how the Ruby object model looks like:
p Developer.class # Classp Class.superclass # Modulep Module.superclass # Objectp Object.superclass # BasicObject
One important thing to understand here is the meaning of self
. The frontend
method is a regular method that is available on instances of class Developer
, but why is backend
method a class method? Every piece of code executed in Ruby is executed against a particular self. When the Ruby interpreter executes any code it always keeps track of the value self
for any given line. self
is always referring to some object but that object can change based on the code executed. For example, inside a class definition, the self
refers to the class itself which is an instance of class Class
.
class Developer p self end# Developer
Inside instance methods, self
refers to an instance of the class.
class Developer def frontend self endend p Developer.new.frontend# #<Developer:0x2c8a148>
Dentro dos métodos de classe, self
refere-se à classe em si de uma forma (que será discutida mais detalhadamente mais adiante neste artigo):
class Developer def self.backend self endendp Developer.backend# Developer
Isso é bom, mas afinal o que é um método de classe? Antes de responder a essa pergunta precisamos mencionar a existência de algo chamado metaclasse, também conhecido como classe singleton e eigenclass. Class method frontend
que definimos anteriormente não passa de um método de instância definido na metaclasse para o objeto Developer
! Uma metaclasse é essencialmente uma classe que o Ruby cria e insere na hierarquia da herança para guardar métodos de classe, não interferindo assim com instâncias que são criadas a partir da classe.
Metaclasses
Todos os objectos em Ruby têm a sua própria metaclasse. Ele é de alguma forma invisível para um desenvolvedor, mas está lá e você pode usá-lo muito facilmente. Uma vez que a nossa classe Developer
é essencialmente um objecto, ele tem a sua própria metaclasse. Como exemplo vamos criar um objecto de uma classe String
e manipular a sua metaclasse:
example = "I'm a string object"def example.something self.upcaseendp example.something# I'M A STRING OBJECT
O que fizemos aqui foi adicionar um método de um único botão something
a um objecto. A diferença entre métodos de classe e métodos singleton é que métodos de classe estão disponíveis para todas as instâncias de um objeto de classe enquanto métodos singleton estão disponíveis apenas para aquela instância única. Métodos de classe são amplamente utilizados enquanto métodos singleton não tanto, mas ambos os tipos de métodos são adicionados a uma metaclasse daquele objeto.
O exemplo anterior poderia ser reescrito assim:
example = "I'm a string object"class << example def something self.upcase endend
A sintaxe é diferente mas efetivamente faz a mesma coisa. Agora vamos voltar ao exemplo anterior onde criámos Developer
class e explorar algumas outras sintaxes para definir um método de classe:
class Developer def self.backend "I am backend developer" endend
Esta é uma definição básica que quase toda a gente usa.
def Developer.backend "I am backend developer"end
This is the same thing, we are defining the backend
class method for Developer
. We didn’t use self
but defining a method like this effectively makes it a class method.
class Developer class << self def backend "I am backend developer" end endend
Again, we are defining a class method, but using syntax similar to one we used to define a singleton method for a String
object. You may notice that we used self
here which refers to a Developer
object itself. First we opened Developer
class, making self equal to the Developer
class. Next, we do class << self
, making self equal to Developer
‘s metaclass. Then we define a method backend
on Developer
‘s metaclass.
class << Developer def backend "I am backend developer" endend
By defining a block like this, we are setting self
to Developer
‘s metaclass for the duration of the block. As a result, the backend
method is added to Developer
‘s metaclass, rather than the class itself.
Let’s see how this metaclass behaves in the inheritance tree:
As you saw in previous examples, there’s no real proof that metaclass even exists. But we can use a little hack that can show us the existence of this invisible class:
class Object def metaclass_example class << self self end endend
If we define an instance method in Object
class (yes, we can reopen any class anytime, that’s yet another beauty of metaprogramming), we will have a self
referring to the Object
object inside it. Podemos então usar class << self
sintaxe para mudar o eu atual para apontar para a metaclasse do objeto atual. Como o objeto corrente é Object
classe em si, esta seria a metaclasse da instância. O método retorna self
que é, neste ponto, uma metaclasse em si. Então chamando este método de instância em qualquer objeto, podemos obter uma metaclasse desse objeto. Vamos definir nosso Developer
classe novamente e começar a explorar um pouco:
E para o crescendo, vamos ver a prova de que frontend
é um método de instância de uma classe e backend
é um método de instância de uma metaclasse:
embora, para obter a metaclasse você não precisa reabrir Object
e adicionar este hack. Pode usar singleton_class
que o Ruby fornece. É o mesmo que metaclass_example
que nós adicionamos mas com este hack pode realmente ver como o Ruby funciona por baixo do hood:
p developer.class.singleton_class.instance_methods false#
Definindo Métodos Usando “class_eval” e “instance_eval”
Existe mais uma forma de criar um método de classe, e que é usando instance_eval
:
Este código Ruby interpreter avalia no contexto de uma instância, que neste caso é um objecto Developer
. E quando você está definindo um método em um objeto você está criando ou um método de classe ou um método de um único botão. Neste caso, é um método de classe – para ser exato, métodos de classe são métodos singleton mas métodos singleton de uma classe, enquanto os outros são métodos singleton de um objeto.
Por outro lado, class_eval
avalia o código no contexto de uma classe ao invés de uma instância. Ele praticamente reabre a classe. Aqui está como class_eval
pode ser usado para criar um método de instância:
Para resumir, quando você chama class_eval
método, você muda self
para se referir à classe original e quando você chama instance_eval
self
muda para se referir à metaclasse da classe original.
Definindo métodos ausentes na mosca
Mais uma peça de metaprogramação é method_missing
. Quando você chama um método em um objeto, Ruby primeiro vai para a classe e navega pelos seus métodos de instância. Se não encontrar o método lá, continua a procurar na cadeia dos antepassados. Se o Ruby ainda não encontrar o método, ele chama outro método chamado method_missing
que é um método de instância de Kernel
que todo o objecto herda. Como temos a certeza que o Ruby vai eventualmente chamar a este método por métodos em falta, podemos usar isto para implementar alguns truques.
define_method
é um método definido em Module
classe que pode usar para criar métodos dinamicamente. Para usar define_method
, chama-se o método com o nome do novo método e um bloco onde os parâmetros do bloco se tornam os parâmetros do novo método. Qual é a diferença entre usar def
para criar um método e define_method
? Não há muita diferença exceto que você pode usar define_method
em combinação com method_missing
para escrever código DRY. Para ser exato, você pode usar define_method
em vez de def
para manipular escopos ao definir uma classe, mas isso é toda uma outra história. Vejamos um exemplo simples:
Isto mostra como define_method
foi usado para criar um método de instância sem usar um def
. No entanto, há muito mais que podemos fazer com eles. Vamos dar uma olhada neste trecho de código:
class Developer def coding_frontend p "writing frontend" end def coding_backend p "writing backend" endenddeveloper = Developer.newdeveloper.coding_frontend# "writing frontend"developer.coding_backend# "writing backend"
Este código não é DRY, mas usando define_method
podemos torná-lo DRY:
Isso é muito melhor, mas ainda não perfeito. Porquê? Se queremos adicionar um novo método coding_debug
por exemplo, precisamos de colocar isto "debug"
no array. Mas usando method_missing
podemos corrigir isto:
Este pedaço de código é um pouco complicado, então vamos quebrá-lo. Chamando um método que não existe irá disparar method_missing
. Aqui, queremos criar um novo método apenas quando o nome do método começa com "coding_"
. Caso contrário, apenas chamamos super para fazer o trabalho de reportar um método que está realmente faltando. E nós estamos simplesmente usando define_method
para criar esse novo método. É isso mesmo! Com este pedaço de código podemos criar literalmente milhares de novos métodos começando com "coding_"
, e esse fato é o que torna o nosso código SECO. Como define_method
é privado para Module
, precisamos de usar send
para o invocar.
Brato para cima
Esta é apenas a ponta do iceberg. Para se tornar um Jedi Ruby, este é o ponto de partida. Depois de dominar estes blocos de construção de meta-programação e compreender verdadeiramente a sua essência, pode prosseguir para algo mais complexo, por exemplo, criar a sua própria Linguagem específica de Domínio (DSL). A DSL é um tópico em si, mas estes conceitos básicos são um pré-requisito para compreender tópicos avançados. Algumas das gemas mais usadas no Rails foram construídas dessa forma e você provavelmente usou sua DSL sem mesmo conhecê-la, como RSpec e ActiveRecord.
esperançosamente este artigo pode lhe dar um passo para entender a metaprogramação e talvez até mesmo construir sua própria DSL, que você pode usar para codificar mais eficientemente.