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_missing não aparecem em methods ou 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 com respond_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_missing extensivamente para implementar find_by_*, find_or_create_by_* e scopes dinâmicos. Quando você chama User.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_errors e respond_to sã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 ... end são definidas em tempo de execução usando define_method internamente. Helpers e filtros também são registrados dinamicamente.

Referências