Auditando Métodos em Ruby

Mais um da série “Coisas que você nunca quis fazer com Ruby, e tinha medo de perguntar”, embora o título não seja tão correto assim: bem ou mal, eu precisei fazer isso algumas vezes em um ou outro trabalho meu, mas enfim, vamos ao cenário:

Imagine que você está mexendo num código de alguém. O código está com acoplamento muito alto, e antes de refatorá-lo você precisa entendê-lo – e é aí que entra o problema. Ruby, como todos sabemos, não tem uma IDE muito boa, com um debugger muito bom, e para fazer esse processo seria legal se, por exemplo, eu pudesse rodar um comando e ele me retornasse: “Entrei no método X, Entrei no método Y, Saí do método Y”, etc.

Bom, o fato é que é bem simples de fazer isso com Ruby:

Para tal, basta “apenas” reabrir todas as classes que formos auditar, puxar todos os métodos que são implementados naquela classe, e re-escrevê-los de forma que seja informado que aquele método foi chamado. Como não há muito desafio em fazer tal coisa, apresentarei duas formas de fazê-lo.

As duas formas são muito semelhantes, e o que se difere é se vai ser criado um novo método (usando alias) ou se não vai ser criado, e vamos aproveitar o conceito de “bindings” e “closures” para fazê-lo. Para começar, vamos criar um método, “auditar”:

def auditar(*classes)
  classes.flatten!
  classes.each do |classe|
    auditar_classe(classe)
  end
end

Até aqui, nenhum segredo: recebo uma lista de classes, e vou chamar o método “auditar_classe” em cada uma delas. O método auditar_classe basicamente se aproveita de um parâmetro do “instance_methods”, do Ruby: se eu passar “false” para o “instance_methods”, ele me trás apenas os métodos que foram definidos (ou re-definidos) naquela classe, e não traz os herdados.

ruby-1.8.7-p334 :002 > Fixnum.instance_methods false
 => ["**", "-", "divmod", "<=>", "even?", "to_s", "id2name", "==", "[]", "/", "abs", "|", "zero?", "div", "%", "<<", "odd?", ">=", "<", "~", "&", ">>", "size", "<=", "^", "fdiv", ">", "*", "modulo", "to_f", "to_sym", "-@", "+", "quo"] 

Ok, então vamos auditar método por método. Isso, claramente, é muito semelhante ao método anterior:

def auditar_classe(classe)
  classe.instance_methods(false).each { |x| auditar_metodo classe, x, "#{classe}#" }
  metaclasse = class << classe; self; end
  classe.methods(false).each { |x| auditar_metodo metaclasse, x, "#{classe}." }
end

Ok… terceira e quarta linha, o que é isso? Se você não sabe, recomendo ler O que é Eigenclass, Afinal?. O ponto é, na terceira linha estamos puxando a eigenclass da “classe” (ou seja, a classe intermediária entre a classe que estamos auditando e a classe Class, da qual todas as classes são instâncias), e no quarto, estamos auditando todos os métodos da eigenclass. O último parâmetro da classe parece estranho, mas a idéia é bem simples: vou concatenar essa string ao nome do método, para que saibamos se estamos chamando “Classe.metodo” ou “Classe#metodo”.

def auditar_metodo(classe, metodo, concatenar)
  classe.send :alias_method, " #{metodo}", metodo
  classe.send :define_method, metodo do |*args, &b|
    STDERR.puts "Entrei no método #{concatenar}#{metodo}"
    res = send " #{metodo}", *args, &b
    STDERR.puts "Saí do método #{concatenar}#{metodo}"
    res
  end
end

Bom, umas coisas importantes: primeiro, estou enviando a mensagem para o STDERR, já que a idéia da auditoria é não ser confundida com a saída padrão do aplicativo (caso haja alguma). Logo depois, estou chamando o método privado “alias_method”, e criando um alias para o método original. O nome do alias, estranhamente, é igual ao nome do método, acrescido de um espaço em branco (sabia que dá pra criar métodos, em Ruby, com caracteres que não são válidos como “keywords”? Isso é bem interessante, se algum dia você quiser sobrescrever o método “!=”, que é possível de se fazer no Ruby 1.9 mas dá um erro de sintaxe no 1.8, eu recomendo usar essa técnica), e usaremos esse nome mais tarde, na linha 5, para chamar o método, junto com todos os seus argumentos, mais um bloco se houver, e guardar o resultado numa variável. O resto é imprimir que eu saí do método, e retornar o resultado da variável.

Relativamente simples. Mas dá pra fazer de um outro jeito, sem usar o “alias”: em Ruby, existe um método em todas as classes chamado “instance_method”. Ele retorna um objeto UnboundMethod que, ao ser preso a uma classe (bind), ele pode ser chamado como se fosse um bloco (Proc). Ou seja, eu posso fazer isso aqui:

metodo = String.instance_method :to_i
metodo.bind("10").call #Retorna 10.

Ou seja, podemos aproveitar isso no nosso código também:

  metodo = classe.instance_method metodo
  classe.send :define_method, metodo.name do |*args, &b|
    STDERR.puts "Entrei no método #{klass}#{metodo.name}"
    res = metodo.bind(self).call(*args, &b)
    STDERR.puts "Saí do método #{klass}#{metodo.name}"
    res
  end

Claro que algumas coisas mudam: Ao invés de simplesmente imprimir “metodo”, eu imprimo “metodo.name”, que define o nome do método. O resto é bem parecido, porém tem a vantagem de não criar métodos adicionais na classe, com o “alias”, e tem a desvantagem de que, em classes com implementação meio bizarra (*COF* rails *COF*) essa abordagem pode dar problemas.

Claro, JAMAIS tente auditar o módulo Kernel (isso vai sobrescrever, basicamente, o “define_method” e o “send”, causando meio que uma reação em cadeia, um loop infinito, e um segfault), mas em linhas gerais, é uma abordagem interessante que funciona muito bem. Para variar, ese com moderação!

Advertisements
This entry was posted in Coisas que você nunca quis fazer com Ruby and tagged . Bookmark the permalink.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s