Autor: Bernardo Monteiro Rufino da Equipe Webly
Licença Creative Commons: Este conteúdo só pode ser copiado caso seja LINKADO para este original e citado o nome do autor antes de qualquer outro texto.
Primeiro vamos definir do que estamos falando, não vamos escrever código que gera outro código. A metaprogramação à qual me refiro é aquela onde criamos construções que definirão elementos no nosso código. Uma maneira de "criar" uma linguagem para resolver problemas (DSL) e não se repetir (DRY).
Por que Ruby? Ruby possui a maioria dos aspectos necessários à metaprogramação: é dinâmica, os elementos estão abertos a mudanças (Mixins), tem blocos de código (Closures, Code Blocks) que permitem definir novas estruturas de controle e também tem sintaxe simples, enxuta, expressiva e legível.
Ok, já falamos demais, vamos à prática, é preciso conhecimento em Ruby para entender os códigos.
Primeiro vamos ver um exemplo nativo da linguagem, attr_accessor, attr_reader e attr_writer, que são atalhos para getters e setters.
class Person attr_reader :name; attr_accessor :hair; def initialize(name, hair) @name, @hair = name, hair; end end jose = Person.new "Jose", :brown; jose.name #=> "Jose" jose.hair #=> :brown jose.hair = :blond #Pintou o cabelo jose.hair #=> :blond
Depois da definição da classe, na segunda linha, estamos chamando métodos da classe Module, não é sintaxe própria da linguagem e sim métodos. Nesse mesmo post reescreveremos esses métodos. Agora vou mostrar algumas técnicas que serão usadas para se fazer isso.
Usando Meta classes
Em Ruby tudo são objetos, e classes são um tipo especial de objeto a diferença é que podem conter métodos, métodos não são armazenados em objetos mas sim em suas respectivas classes, objetos armazenam variáveis de instância. Ainda assim podemos criar métodos particulares a um objeto que não afetam as outras instâncias.
class Dog
def self.bark
"UAU! from #{self.class}";
end
def bark
"UAU! from #{self.class}";
end
end
#Chamando os métodos da classe e dos objetos
Dog.bark #=> "UAU! from Class"
first_dog = Dog.new
first_dog.bark #=> "UAU! from Dog"
second_dog = Dog.new
second_dog.bark #=> "UAU! from Dog"
#(1)Definindo um método particular a um objeto
def first_dog.walk
"Walking..."
end
#O novo método só afeta aquele objeto
first_dog.walk #=> "Walking..."
#second_dog.walk #=> NoMethodError
#Acessando esses métodos
first_dog.singleton_methods #=> ["walk"]
second_dog.singleton_methods #=> []Ué mas para onde estes métodos vão se os objetos não armazenam eles. Esses métodos são armazenados em uma Meta Classe (Metaclass ou Singleton Class) referente a esse objeto e pode ser acessada desse jeito:
metaclass = (class << first_dog; self; end);
Desse jeito poderíamos escrever o método walk, que foi escrito somente para first_dog, em second_dog usando sua meta classe.
class << second_dog def walk "Walking..." end end second_dog.walk #=> "Walking..." second_dog.singleton_methods #=> ["walk"]
Agora parece que já entendemos tudo sobre meta classes. Mas ainda tem um detalhe, como classes são instâncias de Class os métodos de classe (ou estáticos), como o método bark de Dog definido em def self.bark, são armazenados em uma meta classe dessa classe.
#Métodos de classe Dog.singleton_methods #=> [..., "bark"] #Diversas maneiras de se definir métodos de classe, lembre que self se refere a classe class Dog #Usando self def self.foo1; "Foo"; end #Usando o nome Dog def Dog.foo2; "Foo"; end #Usando a meta classe da classe com self class << self def foo3; "Foo"; end end #Usando a meta classe da classe com o nome Dog class << Dog def foo4; "Foo"; end end end Dog.foo1 #=> "Foo" Dog.foo2 #=> "Foo" Dog.foo3 #=> "Foo" Dog.foo4 #=> "Foo" Dog.singleton_methods #=> [..., "bark", "foo1", "foo2", "foo3", "foo4"]
Acredito que com o exemplo acima tudo se esclareceu. Agora vamos tornar o acesso às meta classes mais fácil.
class Object def metaclass class << self self end end end metaclass1 = (class << first_dog; self; end) #O jeito antigo metaclass2 = first_dog.metaclass #Usando o método definido acima metaclass1 == metaclass2 #=> true
Pronto, agora você já sabe pra onde vão os métodos, o que são meta classes e para que servem.
Definindo métodos
Definir métodos dinamicamente é fácil, Ruby nos trás o método define_method para isso. Veja este exemplo abaixo:
class Pig define_method :count do @count = (@count || 0) + 1; end end pig = Pig.new pig.count
Esse exemplo não mostrou muita coisa nova, já que poderíamos ter usado a palavra chave def. Mas esse método pode nos oferecer muitas vantagens, primeiro ele pode ser usado para criar métodos dinamicamente já que aceita um Symbol ou String como parâmetro para nome do método e segundo que aceita bloco de código para seu conteúdo. Poderíamos fazer isso avaliando uma String, mas isso é perigoso e muito menos "elegante".
Para chamá-lo fora da definição da classe (ou do modulo), temos que usar o método send e como parâmetro :define_method, ou então usar class_eval (ou module_eval que é igual), mas veremos isso depois. A visibilidade do método que ele irá criar será de acordo com a visibilidade atual, em top-level a visibilidade é private, então ele criará métodos privados até que declaremos public, por isso que se você usá-lo dentro de classes onde a visibilidade é publica por padrão os métodos definidos serão públicos.
Counter = Class.new
shared_count = 0 #Uma variável local que será compartilhada pelo método
public #Se não declarassemos o método abaixo seria privado
Counter.send(:define_method, :count) do
shared_count += 1;
@instance_count = (@instance_count || 0) + 1;
{:shared => shared_count, :instance => @instance_count}
end
private #Voltamos à visibilidade padrão
first_counter = Counter.new
second_counter = Counter.new
first_counter.count #=> {:shared=>1, :instance=>1}
second_counter.count #=> {:shared=>2, :instance=>1}
first_counter.count #=> {:shared=>3, :instance=>2}
second_counter.count #=> {:shared=>4, :instance=>2}Vimos nesse exemplo que podemos compartilhar o contexto através do bloco de código e que o método criado terá visibilidade igual à atual. Agora iremos declarar métodos só para um objeto usando as meta classes que já estudamos, note que nesse caso irei utilizar class_eval, assim além de não precisar usar send também não precisarei declarar public já que dentro já é publico.
#Usando class_eval, não precisamos declarar public
first_counter.metaclass.class_eval do
define_method(:instance_count){@instance_count}
end
first_counter.instance_count #=> 2
#second_counter.instance_count #=> NoMethodErrorTambém podemos fazer em módulos, por que não? Abaixo mostro como podemos usar define_method dentro de module_eval também. Usamos extend para adicionar métodos de instância a um objeto quando usado com esse objeto ou para adicionar métodos de classe a uma classe quando usada com essa classe. Usamos include para adicionar métodos de instância de alguma classe quando usada com essa classe.
class Man
def walk
"Walking..."
end
include Runner
end
Runner.module_eval do
define_method(:run){"Running..."}
end
woman = Object.new
woman.extend Runner
man = Man.new
woman.run #=> "Running..."
man.walk #=> "Walking..."
man.run #=> "Running..."Acessando e modificando variáveis de instância
O truque que Rails usa para deixar as variáveis de instância do controller acessíveis na view é usar os métodos que Ruby nos oferece para manipular variáveis de instância, são eles instance_variables, instance_variable_get e instance_variable_set. Isso serve para copiar variáveis de um objeto à outro, o exemplo é simples de entender.
class FirstClass
attr_reader :first, :second;
def initialize
@first = "First First";
@second = "Second First";
@first_uniq = "FirstClass"
end
end
class SecondClass
attr_reader :first, :second;
def initialize
@first = "First Second";
@second = "Second Second";
@second_uniq = "SecondClass"
end
end
first = FirstClass.new
second = SecondClass.new
first.instance_variable_get("@first") #=> "First First"
first.instance_variable_get(:@second) #=> "Second First"
second.instance_variables #=> ["@second_uniq", "@first", "@second"]
second.first #=> "First Second"
second.instance_variable_set(:@first, "First Outside")
second.first #=> "First Outside"
second.instance_variable_set(:@first, "Second Outside")
#Copying the instance variables from 'second' to 'first'
for var in second.instance_variables
first.instance_variable_set(var, second.instance_variable_get(var))
end
#Iterating the array with the variables and printing them
for var in first.instance_variables.sort
puts "#{var} = #{first.instance_variable_get(var).inspect}"
end
#@first = "Second Outside"
#@first_uniq = "FirstClass"
#@second = "Second Second"
#@second_uniq = "SecondClass"Diferentes versões de eval
Existem diversas versões de comandos eval em Ruby. São elas eval, instance_eval, class_eval e module_eval, lembrando que module_eval é o mesmo que class_eval. A primeira, eval só aceita String como argumento para ser executado, enquanto os outros aceitam um bloco de código, isso quer dizer que você só deve utilizar eval como último caminho, eval sempre executará no contexto atual. O exemplo abaixo demonstra bem as diferenças entre as outras.
Cat = Class.new Cat.class_eval do def from_class_eval "From Cat::class_eval" end end Cat.module_eval do def from_module_eval "From Cat::module_eval" end end Cat.instance_eval do def from_instance_eval "From Cat::instance_eval" end end cat = Cat.new cat.instance_eval do def from_instance_eval "From cat.instance_eval" end end cat.from_class_eval #=> "From Cat::class_eval" cat.from_module_eval #=> "From Cat::module_eval" cat.from_instance_eval #=> "From cat.instance_eval" #Cat.from_class_eval #=> NoMethodError #Cat.from_module_eval #=> NoMethodError Cat.from_instance_eval #=> "From Cat::instance_eval"
Declarações de classe
Quando se define uma classe em Ruby o código dentro da definição não é estático, podemos fazer comparações, usar estruturas de controle, etc... Assim como chamar métodos de suas super classes. É isso que iremos utilizar para dar um exemplo do que foi utilizado no Rails e o que pode se chamar de linguagem especifica de domínio (DSL). Olhe o exemplo abaixo e veja como é fácil criar uma "declaração", que na verdade é uma chamada de método:
class Product #Definindo um método de classe da classe que herda de Product chamado category def self.set_category(cat) #Definindo métodos dentro da metaclasse que serão da classe self.metaclass.class_eval do define_method(:category) do cat end end end end class Camera < Product set_category "eletronicos" end Camera.category #=> "eletronicos"
Assim como os métodos de classe das superclasses estarão disponíveis como se fossem declarações, os métodos das instâncias, que podem ser módulos ou classes, de Module ou Class também, lembrando que Class herda de Module. Olhe o exemplo abaixo que demonstra isso reescrevendo o atalho attr_accessor:
class Module
def attr_accessor_personal(*attrs)
attrs.each do |attr|
attr = attr.to_s;
#Definindo métodos dentro da classe que serão da instância
self.class_eval do
#Defininfo o getter -> objeto.atributo
define_method(attr) do
#Pegando a variável de instancia -> @atributo
instance_variable_get("@"+attr);
end
#Defininfo o setter -> objeto.atributo = "valor"
define_method(attr+"=") do |value|
#Setando a variável de instancia -> @atributo = "valor"
instance_variable_set("@"+attr, value);
end
end
end
end
end
class Car
attr_accessor_personal :model, :factory
def initialize(model, factory)
@model, @factory = model, factory;
end
end
car = Car.new "Enzo", "Ferrari"
car.model #=> "Enzo"
car.factory #=> "Ferrari"
car.model = "Gallardo"
car.factory = "Lamborghini"
car.model #=> "Gallardo"
car.factory #=> "Lamborghini"Nos exemplos acima deu para perceber o quanto podemos modificar de classes e objetos.
Usando 'method_missing'
Uma das grandes utilidades de Ruby é method_missing, que permite executar algum código quando o objeto não responde a algum método. Isso simplifica e muito as coisas. No exemplo abaixo uma extensão à classe Hash.
class Hash
alias_method :method_missing2, :method_missing;
def method_missing(method, *args, &block)
method = method.to_s;
if method =~ /(.*)=$/ && self[$1]
self[$1] = args[0];
elsif self[method] && args.empty?
self[method]
else
method_missing2(method, *args, &block);
end
end
end
hash = {"nome" => "Bernardo Rufino", "email" => "bermonruf@gmail.com"}
hash.nome #=> "Bernardo Rufino"
hash.email #=> "bermonruf@gmail.com"
hash.nome = "Alguem Silva"
hash.email = "mail@hotmail.com"
hash.nome #=> "Alguem Silva"
hash.email #=> "mail@hotmail.com"Outro exemplo pode ser visto no XMLBuilder que cria documentos XML assim, e é usado no Rails.
Pra quem quiser ver os exemplos no editor de sua preferência pode baixar o arquivo contendo todos eles em metaprogramacao_em_ruby.rb.
Fontes:
- Ruby Metaprogramming Techiniques - Ola Bini
- Seeing Metaclasses Clearly - Why The Lucky Stiff
- Ruby Metaprogramming Introduction - Practical Ruby

Entrar
Cadastre-se
Ajuda
Responder

Quote