Todo o código incluído neste texto foi projetado para ser executado interativamente no interpretador irb. Você pode copiar os trechos de código daqui para o interpretador na ordem em que aparecem e tudo deve funcionar como esperado. Mas digo logo que eu não garanto que isso aconteça, então use por sua conta e risco.
Todo mundo que programa em Ruby já deve ter definido métodos em classes singleton ou, para usar um termo mais na moda, em eigenclasses. Mesmo quem não sabe o que diabos é isso muito provavelmente já as usou sem saber. Até há bem pouco tempo atrás eu não tinha a mínima idéia da existência dessas coisas, mas um dia precisei usá-las. Mais ou menos como todo mundo que programa em Haskell acaba um dia escrevendo um tutorial sobre Monads, todo mundo que tenta um pouco de metaprogramação em Ruby acaba escrevendo seus próprios artigos sobre eigenclasses. Essa é a minha tentativa de tornar o assunto um pouco menos obscuro para mim mesmo e para os demais. Depois eu conto o que estava querendo fazer, vamos primeiro tentar entender essas classes diferentes.
Primeiramente, as classes singleton não têm muito a ver com o padrão Singleton. O termo provavelmente foi escolhido pela alusão à unicidade, mas as similaridades páram por aí. Classes singleton são classes que, como no padrão, estão associadas a um único objeto. Porém, diferente da implementação comum do padrão, o objeto não é instância dela, mas de outra classe. O maior problema com o termo "classe singleton" é a confusão com o nome do padrão, por isso muita gente tem procurado usar outros termos, como o diferentíssimo eigenclass (que até hoje eu não sei se deu ou roubou o nome do blog do Mauricio Fernandez).
Em Ruby todo objeto possui uma eigenclass associada (para ser realmente preciso, alguns
objetos não possuem, mas vamos assumir que não sabemos disso por enquanto). Os
métodos definidos nesta classe especial ficam disponíveis somente para o objeto
associado e para nenhuma outra instância da mesma classe. Para obter acesso a
ela, pode-se usar a expressão "class << obj; self; end" (onde
obj é o objeto do qual se quer a eigenclass). Quem pretende usar este
tipo de metaprogramação com freqüência pode fazer
como o why
e guardar esta expressão num método em um arquivo qualquer para referência
futura:
class Object
def eigenclass
class << self; self; end
end
end
A eigenclass é única para cada objeto e diferente da classe a partir da qual o objeto foi criado.
a = Object.new
b = Object.new
a.class == Object # => true
a.eigenclass == a.class # => false
a.eigenclass == b.eigenclass # => false
a.eigenclass # => #<Class:#<Object:0xb7ced878>>
Isso quer dizer que métodos adicionados e/ou modificados a estas classes estarão
disponíveis para um objeto apenas. Isso é usado com alguma freqüência para
definir métodos de classe, algo que todo mundo já fez uma vez na vida. Classes
são objetos também, instâncias da classe Class, e possuem uma eigenclass
associada como a maioria dos objetos em Ruby. Por isso podemos definir métodos
de classe:
MyClass = Class.new
MyClass.eigenclass.send :define_method, :instance_count do
@count || 0
end
MyClass.eigenclass.send :define_method, :instance_count_inc do
@count = instance_count + 1
end
MyClass.send :define_method, :initialize do
self.class.instance_count_inc
end
a, b = MyClass.new, MyClass.new
MyClass.instance_count # => 2
Muito para engolir de uma vez só? Eu também achei no início, mas descobri que
fica mais fácil quando se tenta entender uma coisa de cada vez. A primeira é o
método define_method da classe Module (lembre que a classe Class herda de
Module, portanto este método também está disponível para classes). Este método
pode ser usado no lugar da palavra-chave def para (re)definir métodos
de instância dinamicamente. Ele recebe um nome e um bloco para ser
usado como corpo do método. Este é um método privativo, ou seja, só pode ser chamado sobre um
objeto por ele mesmo. Para contornarmos esta limitação usamos o send (por
isso ele aparece ali em cima).
Uma sutileza do define_method é que ele define métodos acessíveis a partir
das instâncias da classe, não da classe em si. A classe MyClass é instância de outra
classe diferente (a classe Class, como está explícito na primeira linha do bloco de código anterior) e se quisermos definir um método para ela,
podemos chamar o define_method em Class:
Class.send :define_method, :instance_count do
@count || 0
end
O problema dessa abordagem é que define o método para todas as instâncias da classe Class.
MyClass.instance_count # => 2
Object.instance_count # => 0
Module.instance_count # => 0
Obviamente existem instâncias da classe Object (posso contar pelo menos sete só nos dois trechos de código acima), mas o contador de instâncias não funciona direito porque precisa ser incrementado durante a inicialização dos objetos. Isso pode causar um pouco de confusão e polui o código das classes que não precisam do recurso de contagem. Uma coisa que realmente não queremos é poluir os programas dos outros sem necessidade, então é melhor darmos um jeito de definir o método somente para as classes em que vamos usá-lo.
O problema é que se chamarmos define_method nas classes em si, estaremos
definindo métodos para as instâncias delas e o contador não vai ter muita
utilidade. Por outro lado, se usarmos o define_method de Class, vamos
injetar código em classes demais. Para nos tirar dessa sinuca de bico, eis que surge a magnânima eigenclass. Antes de entender o que está acontecendo, vamos limpar a
sujeira:
Class.send :remove_method, :instance_count
Object.instance_count # NoMethodError
MyClass.instance_count # => 2
Como pode ver, isso remove o método instance_count de todas as classes, exceto
da MyClass. Ela é poupada porque tem o método definido na sua eigenclass, uma
classe intermediária que não aparece na hierarquia de classes, mas que é
consultada antes da classe real e das superclasses quando uma chamada de método
é recebida. Por isso que no trecho de código original em que definimos o
instance_count as duas primeiras definições são solicitadas à eigenclass, não
à classe. Por outro lado, a última definição é feita para a própria classe
MyClass, para redefinir o método de inicialização de suas instâncias de modo a
aumentar o contador.
Tudo isto poderia ter sido feito com o uso da palavra-chave def e do atalho
def objeto.nome_do_metodo, mas resolvi usar define_method porque ilustra
como gerar métodos dinamicamente (e também porque é mais divertido, ora bolas).
A palavra-chave def pode ser usada para definir métodos em qualquer objeto
(não só classes) e o define_method também, mas por que alguém em sã
consciência iria querer fazer isso?
Bom, há várias razões, mas vou descrever apenas o uso que citei no início do texto. No Mediacloth há uma classe responsável por resolver nomes de página wiki para URLs que geralmente é usada somente para ter um dos métodos sobre-escritos. Sem a magia negra de eigenclass os clientes vão normalmente estender esta classe e definir um trecho de código personalizado para este método:
class MyLinkHandler < MediaWikiLinkHandler
def link_attributes_for(page)
{ :href => "http://example.com/wiki/#{page}", :class => "internal" }
end
end
generator = MediaWikiHTMLGenerator.new
generator.link_handler = MyLinkHandler.new
Não seria bom se o resolvedor de referências pudesse ser definido com uma chamada de construtor apenas?
generator = MediaWikiHTMLGenerator.new
generator.link_handler = MediaWikiLinkHandler.new do |page|
{ :href => "http://example.com/wiki/#{page}", :class => "internal" }
end
Esse efeito pode ser obtido com uma alteração simples no initialize da classe
original. O código fica parecido com o que está aí embaixo:
class MediaWikiLinkHandler
def initialize(&block)
eigenclass.send(:define_method, :link_attributes_for, block) if block
end
end
Com Ruby 1.9 tudo isso vai ficar muito mais fácil, já que o método
define_singleton_method (que já é usado por muita gente) será incluído na
distribuição padrão, como o
Taq já notou. De qualquer modo, este
exercício ainda continua válido como forma de aprendizado. É para isto que
estamos aqui, não?
