Metaprogramação em Ruby: como e quando usar method_missing e define_method
1. Fundamentos da Metaprogramação em Ruby
Metaprogramação é a técnica de escrever código que escreve código durante a execução. Ruby é uma linguagem particularmente favorável a essa abordagem devido à sua natureza dinâmica: classes podem ser abertas e modificadas a qualquer momento, objetos podem ganhar novos métodos em tempo real, e a introspecção é parte fundamental da linguagem.
Diferente de linguagens compiladas como Java ou C++, onde a estrutura de classes é definida em tempo de compilação, Ruby permite que você defina, redefina e remova métodos durante a execução do programa. Ferramentas como send, respond_to? e define_method são os blocos de construção básicos dessa flexibilidade.
2. method_missing: O Gancho Universal para Métodos Inexistentes
O method_missing é um método privado da classe BasicObject que é invocado sempre que um método chamado não é encontrado na cadeia de herança do objeto. Implementá-lo permite capturar essas chamadas e tratá-las dinamicamente.
class Saudacao
def method_missing(method_name, *args, &block)
if method_name.to_s.start_with?("diga_")
saudacao = method_name.to_s.split("_").last
puts "#{saudacao.capitalize}, #{args.first}!"
else
super
end
end
def respond_to_missing?(method_name, include_private = false)
method_name.to_s.start_with?("diga_") || super
end
end
s = Saudacao.new
s.diga_ola("Maria") # Saída: Ola, Maria!
s.diga_tchau("João") # Saída: Tchau, João!
Note que é essencial redefinir também respond_to_missing? para que respond_to? funcione corretamente com os métodos dinâmicos.
3. Casos de Uso Práticos de method_missing
Delegação dinâmica: criar proxies que delegam chamadas para objetos internos.
class ProxyDeLog
def initialize(alvo)
@alvo = alvo
end
def method_missing(method_name, *args, &block)
puts "[LOG] Chamando #{method_name} com #{args}"
@alvo.send(method_name, *args, &block)
end
def respond_to_missing?(method_name, include_private = false)
@alvo.respond_to?(method_name) || super
end
end
class Calculadora
def soma(a, b)
a + b
end
end
calc = Calculadora.new
proxy = ProxyDeLog.new(calc)
proxy.soma(2, 3) # Loga e retorna 5
DSLs simples: construir APIs fluentes para configuração.
class Configuracao
def method_missing(nome, *args)
if args.empty?
instance_variable_get("@#{nome}")
else
instance_variable_set("@#{nome}", args.first)
end
end
end
config = Configuracao.new
config.email "user@example.com"
config.porta 3000
puts config.email # user@example.com
4. define_method: Criando Métodos Dinamicamente
Diferente de method_missing, que intercepta chamadas a métodos inexistentes, define_method cria métodos reais no objeto. Isso é feito em tempo de execução, mas os métodos passam a existir formalmente.
class CriadorDeMetodos
[:ola, :tchau, :oi].each do |nome|
define_method(nome) do |pessoa|
puts "#{nome.capitalize}, #{pessoa}!"
end
end
end
c = CriadorDeMetodos.new
c.ola("Ana") # Ola, Ana!
c.tchau("Pedro") # Tchau, Pedro!
Uma vantagem crucial do define_method é que ele captura closures — variáveis do escopo onde foi definido permanecem acessíveis.
5. Padrões Avançados com define_method
Geração automática de getters/setters:
class MeuAccessor
def self.meu_attr_accessor(*nomes)
nomes.each do |nome|
define_method(nome) do
instance_variable_get("@#{nome}")
end
define_method("#{nome}=") do |valor|
instance_variable_set("@#{nome}", valor)
end
end
end
meu_attr_accessor :nome, :idade
end
pessoa = MeuAccessor.new
pessoa.nome = "Carlos"
pessoa.idade = 30
puts pessoa.nome # Carlos
Métodos de classe dinâmicos com define_singleton_method:
class FabricaDeMetodos
def self.criar_metodo(nome, &bloco)
define_singleton_method(nome, &bloco)
end
end
FabricaDeMetodos.criar_metodo(:saudacao) { |nome| "Olá, #{nome}!" }
puts FabricaDeMetodos.saudacao("Ana") # Olá, Ana!
6. Quando Evitar Metaprogramação: Armadilhas e Boas Práticas
- Depuração: métodos criados por
method_missingnão aparecem emmethodsou na stack trace de forma clara. Ferramentas de debug podem se confundir. - Performance:
method_missingé significativamente mais lento que métodos reais. Cada chamada percorre a cadeia de herança até encontrar o gancho. Para mitigar, combine comrespond_to?para evitar lookup desnecessário. - Legibilidade: código metaprogramado pode ser difícil de entender. Documente claramente e limite o escopo da "magia" a módulos bem definidos.
# Comparação de performance (conceitual)
class ComMethodMissing
def method_missing(nome, *args)
if nome == :soma
args.sum
else
super
end
end
end
class ComMetodoReal
define_method(:soma) { |*args| args.sum }
end
7. Combinando method_missing e define_method na Prática
A estratégia híbrida é poderosa: use method_missing para capturar chamadas iniciais e define_method para criar o método permanentemente, eliminando a sobrecarga futura.
class ProxySobDemanda
def initialize(alvo)
@alvo = alvo
@metodos_criados = []
end
def method_missing(nome, *args, &block)
if @alvo.respond_to?(nome) && !@metodos_criados.include?(nome)
self.class.define_method(nome) do |*a, &b|
@alvo.send(nome, *a, &b)
end
@metodos_criados << nome
@alvo.send(nome, *args, &block)
else
super
end
end
def respond_to_missing?(nome, include_private = false)
@alvo.respond_to?(nome) || super
end
end
api = ProxySobDemanda.new(Calculadora.new)
api.soma(1, 2) # Primeira chamada: method_missing + define_method
api.soma(3, 4) # Segunda chamada: método real, sem overhead
Testabilidade: para testar código metaprogramado, use respond_to? para verificar a existência de métodos e teste os comportamentos dinâmicos como casos de borda.
8. Metaprogramação no Ecossistema Ruby: Gems e Frameworks
- ActiveRecord (Rails): usa
method_missingextensivamente para implementarfind_by_*,find_or_create_by_*e scopes dinâmicos. Quando você chamaUser.find_by_email("test@test.com"), o ActiveRecord intercepta a chamada, analisa o nome do método e constrói a query SQL apropriada. - RSpec: matchers como
be_valid,have_errorserespond_tosão gerados dinamicamente. A DSL de descrição (describe,context,it) também usa metaprogramação para criar exemplos e grupos. - Sinatra: rotas como
get '/hello' do ... endsão definidas em tempo de execução usandodefine_methodinternamente. Helpers e filtros também são registrados dinamicamente.
Referências
- Ruby-Doc: method_missing — Documentação oficial do método
method_missingna classe BasicObject - Ruby-Doc: define_method — Documentação oficial de
define_methodno módulo Module - Ruby Metaprogramming: A Complete Guide — Artigo técnico detalhado sobre metaprogramação em Ruby no Toptal
- Understanding Ruby's method_missing and respond_to_missing? — Guia prático do Thoughtbot sobre boas práticas com method_missing
- Metaprogramming in Ruby: It's All About the Self — Livro referência sobre metaprogramação em Ruby da Pragmatic Bookshelf