Hooks em Ruby
Ruby é uma linguagem dinâmica e utiliza algumas caracteristicas interessantes para manipular objetos e classes no seu ciclo de vida em run-time, como por exemplo usar singleton methods em um objeto, realizar mixin, adicionar métodos em uma classe e outras coisas legais. Para capturar esse eventos que alteram o comportamento de classes, módulos e objetos, Ruby provê alguns Hooks.

O que seriam Hook propriamente dito(pergunta clássica no curso de metaprogramação)? Hooks são métodos com um nome específico para capturar um mudança no objeto. Esses hooks podem ser definidos por uma API ou mesmo pela a especificação da linguagem. Um exemplo de API que utilize hooks são os callbacks do ActiveRecord, como after_save, before_save, etc, esses métodos são invocados automaticamente após a chamada “save” do model como descrito na especificação.
Hooks da Linguagem Ruby
O intuito deste poste é falar sobre alguns hooks da linguagem Ruby, vamos iniciar capturando o evento de herança de uma classe, isso pode ser muito útil se sua super-classe precisa conhecer a subclass. Para esse hook funcionar temos que definir o método self.inherited na superclass. Exemplo
class A
def self.inherited(subclass)
puts "Eu #{self} sendo herdada por #{subclass}"
end
end
class B < A
end
#=> "Eu A sendo herdada por B"
O conteúdo do método inherited é executado pelo interpretador no momento em que a sub classe é carregada na vm. Lembra do final public class A{} em java? Para quem nunca usou java o final public class é forma de não permitir que uma classe seja herdada por outra. Em ruby você pode simular esse comportamento lançando uma exceção dentro do self.inherited, veja:
class A
def self.inherited(subclass)
raise "A classe #{self} nao pode ser herdada!"
end
end
class B < A
end
#E=> RuntimeError: A classe A nao pode ser herdada!
self.included
O self.inherited é usado apenas para classes, se você precisar detectar mix-in entre módulos e classes, defina o método self.included dentro do módulo que será mixado. O exemplo abaixo demonstra como capturar o evento de mix-in(include):
module M
def self.included(from)
puts "#{self} foi mixado por #{from} "
end
end
class A
include M
end
Particularmente acho o self.included muito útil, com ele podemos adicionar funcionalidades durante o mix-in. Por exemplo, vamos criar um plugin chamado Printable, na verdade é só um módulo com um pouco de metaprogramação para detectar os accessor(setters e getters) e definir um novo método com o prefixo print_ para imprimir o retorno do método original, seria basicamente assim.
class Person attr_accessor :name, :code end p = Person.new p.name ="Foobar" p.print_name #ainda não existe
Vamos detectar os accessor, se um método tem dois métodos com o mesmo nome mas sendo um terminado com o operador de atribuição(=) então é um acessor.
Antes vamos criar a funcionalidade apenas para classe Person
class Person
attr_accessor :name, :code
end
#pegando os métodos de instância
methods = Person.instance_methods false
#=> ["code", "name=", "code=", "name"]
#detectando quais deles são accessors
acessors=methods.select{|m| methods.include? "#{m}=" }
#=> ["code", "name"]
#definindo os métodos na classe
acessors.each do |acessor|
Person.send :define_method, "print_#{acessor}" do
puts "Printing #{send(acessor)}..."
end
end
#Testando
p = Person.new
p.name = "FFUUUU"
p.code = 44444
p.print_name #=> Printing FFUUUU ...
p.print_code #=> Printing 44444 ...
Perfeito! Agora vamos criar um módulo e utilizar o hook self.included, com isso, quando uma classe necessitar dessa funcionalidade é só fazer o include de Printable, veja
module Printable
def self.included(klazz)
methods = klazz.instance_methods false
acessors=methods.select{|m| methods.include? "#{m}=" }
acessors.each do |acessor|
klazz.send :define_method, "print_#{acessor}" do
puts "Printing #{send(acessor)}..."
end
end
end
Fazendo mix-in em Person e em OtherClass
class Person
attr_accessor :name, :code
include Printable
end
class OtherClass
attr_accessor :a, :b
include Printable
end
Testando
p = Person.new p.name = "FOO" p.print_name obj = OtherClass.new obj.a = "Hey Everybody!" obj.b = "It's working" obj.print_a obj.print_b
Output
Printing FOO... Printing Hey Everybody!... Printing It's working...
self.method_added
Se você leu esse post até aqui provavelmente você conhece o method_missing, aquele que captura a nome do método e os parâmetros caso o objeto não responda a esse método. Pois é, não falaremos do method_missing nesse post, vamos falar do primo dele, o self.method_addded. O self.method_added é um hook que captura o evento quando um método é adicionado ao objeto ou classe. Exemplo.
class Foo
def self.method_added(method)
puts "Adicionado o metodo #{method} na classe #{self}"
end
def testing
puts "testing"
end
end
#=> "Adicionado o metodo testing na classe Foo"
Veja que na própria classe o evento é disparado. Em uma herança, o mesmo comportamento
class Foo
def self.method_added(method)
puts "Adicionado o metodo #{method} na classe #{self}"
end
end
class Bar < Foo
def other_method
puts "say hello"
end
end
#=> Adicionado o metodo other_method na classe Bar
Ou ainda, você pode capturar singleton methods com o mesmo hook.
class Foo
def self.method_added(method)
puts "Adicionado o metodo #{method} na classe #{self}"
end
end
foo = Foo.new
def foo.new_method
puts "hey I'm adding ..."
end
Object.const_missing
Sabemos que o method_missing captura o evento quando o método não existe, podemos ter uma funcionalidade semelhante para constantes, com o Object.const_missing. Sempre que uma constante não for encontrada na vm, o const_missing recebe o nome da constante. Esse método deve ser definido em Object, dado que constantes o escopo é global. Veja
UmaConstante #E=> NameError: uninitialized constant UmaConstante
def Object.const_missing(const)
puts "Hey nao encontrei #{const} mas nao vou lançar error"
end
UmaConstante
#=> Hey nao encontrei UmaConstante mas nao vou lanar error
Embora isso seja uma coisa legal, se você apenas quer capturar o evento, uma boa prática é fazer a sua lógica usando a constante que está faltando e depois usar o super para lançar a exceção via Kernel. Portando algo como
UmaConstante #E=> NameError: uninitialized constant UmaConstante
def Object.const_missing(const)
puts "Hey nao encontrei #{const} "
super
end
UmaConstante
#=> Hey nao encontrei UmaConstante
#=> NameError: uninitialized constant UmaConstante
#=> from (irb):119:in `const_missing'
#=> from (irb):122
#=> from :0
Se seu é convencionado utilizando o nome do arquivo em snake case e o nome da classe em camel, você pode fazer um plugin para carregar seus arquivos automaticamente caso a classe não esteja visível no trecho do seu código. Exemplo, no mesmo diretório 3 arquivos
Arquivo person.rb
class Person attr_accessor :name, :state end
Arquivo state.rb
class State attr_accessor :code end
Arquivo principal main.rb
p = Person.new p.state = State.new
Rode com
ruby main.rb
De cara o erro
#E=> main.rb:1: uninitialized constant Person (NameError)
Pois não colocamos o requires necessários para cada arquivo.
Vamos fazer um algoritmo para carregar esses arquivos automaticamente usando Kernel.autoload.
Então no main.rb fica com o conteúdo:
def Object.const_missing(const)
#convertendo CamelCase to snake_case
filename = const.to_s.gsub(/([A-Z])/, ' \1').split(' ').join('_').downcase
#Associando a constante ao nome do arquivo
autoload const, filename
#binding a nova constante
Object.const_get const
end
p = Person.new
p.state = State.new
Se você rodar novamente o main.rb vai funcionar.
Conclusão
No dia-a-dia e em projetos pequenos não é comum capturar eventos no nível de interpretação mas se você esta depurando um sistema ou mesmo desenvolvendo uma API, Hooks podem ser essenciais.