Ok, aqui vamos nós para mais estudos de como fazer coisas bizarras em Ruby… outro dia, estava olhando para um livro, “Design Patterns in Ruby”, que falava de classes abstratas (tipo Java) e a inexistência delas em Ruby, aonde “Duck Typing” resolve. Mas aí pensei: será que não dá para simular o comportamento de classes abstratas tipo Java, em Ruby?
Para os que não conhecem Java: se você, em Java, declarar uma classe como abstrata, e definir, digamos, dois métodos abstratos, quando esta classe for herdada, você é obrigado a definir estes dois métodos, senão o código sequer compila. Bom, parece então simples, em Ruby: basta criar uma classe, informar, de alguma que ela é abstrata, e então quando ela for herdada, se a classe herdada não definir todos os métodos, lançar uma exceção (digamos, um NoMethodError). Ok, Ruby permite traçar quando uma classe foi herdada com o método “callback” inherited, portanto é relativamente simples saber se a classe foi herdada e se ela implementa os métodos da abstrata, certo?
Bom, na prática… não.
Para ilustrar melhor, vamos fazer um pequeno exercício: tente fazer isso no Ruby:
class Pai def self.inherited(filho) puts "Fui herdado pelo #{filho}" end end puts "Antes da declaração do filho" class Filho < Pai puts "Antes do END do filho" end puts "Depois do END do filho"
Você verá que aparece o seguinte na tela:
Antes da declaração do filho Fui herdado pelo Filho Antes do END do filho Depois do END do filho
Ou seja, o Ruby chama o callback “inherited” logo depois da linha “class Filho < Pai". Só que, neste ponto, NENHUM método foi definido ainda, e o que queremos é justamente o contrário: queremos rodar um comando logo depois que a classe INTEIRA foi definida. Felizmente, há uma solução: o método set_trace_func, do Ruby, permite traçar certas chamadas do Ruby para código C (como, por exemplo, definição de classe). Este método espera um "Proc", com os seguintes parâmetros: "event", "file", "line", "id", "binding", e "classname" (SEIS parâmetros para um método? Cadê o "Clean Code", Ruby?). Na prática, precisaremos apenas do primeiro, "event" que quando for "class" indica que estamos criando uma classe (ou module), e o "end" indica que estamos fechando a definição da classe. Mais informações sobre o método, procure no ApiDock
Ok, SHOW ME THE CODE! Basicamente, para tentar tratar coisas tipo um module sendo definido dentro do escopo da classe, ou coisas assim, vamos criar um contador para ver o número de “event == ‘class'” que recebemos. Chamei o método de “trace_class_creation”, e a implementação dele é a seguinte:
def trace_class_creation(&block) classes_count = 0 #No início, não há classe nenhuma sendo definida procedure = proc do |event, file, line, id, binding, classname| #Criamos o Proc classes_count += 1 if event == 'class' #Se a linha é "class Algo" ou "module Algo" classes_count -= 1 if event == 'end' #Se a linha está fechando a definição da classe if event == 'end' && classes_count == 0 set_trace_func nil #Pára de traçar os métodos do Ruby block.call #Chama o bloco end end set_trace_func procedure end
Tente usar o código acima! Basta colocar algo assim:
trace_class_creation do puts "Classe Criada!!!" end class Algo module Nada puts "Antes do END do módulo" end puts "Antes do END da classe" end #Aqui, irá exibir "Classe Criada!!!"
Bom, bom, e agora? Como fazer uma classe abstrata? Basta criar uma classe com um método “inherited” que precisa traçar a criação da outra classe, e ver se ela implementa todos os métodos. Para tal, usarei um module. Há uma coisa interessante, entretanto: o método “inherited” DEVE ser definido na classe que será herdada, então:
#Isto funciona: class Pai def self.inherited(filho) puts "Fui herdado" end end #Mas isto não funciona module Herdada def inherited(filho) puts "Fui herdado" end end class OutroPai extend Herdada end
Logo, a solução, a meu ver, é usar outro “callback”, dessa vez no module: “included”. Então, quando o módulo for incluído em uma classe, ele define o método “inherited” da classe, e faz toda a mágica que queremos. O resultado é mais ou menos o seguinte:
module Abstract def self.included(included_class) #Quando eu for incluído na classe "included_class" metaclass = class << included_class; self; end #Puxo a "metaclass" da "included_class" #Define o método "inherited" dentro da metaclass. #este código é o equivalente a fazer "def self.inherited(inherited_class) dentro da classe #apontada pela variável included_class. metaclass.send :define_method, :inherited do |inherited_class| ... end end end
Dentro do método inherited, o que precisamos fazer é (relativamente) simples: precisamos puxar todos os métodos implementados pela “included_class”, traçar a criação da classe herdada (apontada pela “inherited_class”) e quando ela for criada, ver também quais foram os métodos definidos pela classe herdada. Depois, comparar para ver se TODOS os métodos da “included_class” estão na “inherited_class”. Isto é relativamente simples, basta usar os método “public_instance_methods”, “private_instance_methods” e “protected_instance_methods”, passando o argumento “false” para eles (quando você passa “false” para qualquer um destes métodos, eles retornam tudo o que foi definido dentro desta classe, apenas, e não foi trazido por includes ou heranças). Logo, o código completo é:
module Abstract def self.included(included_class) metaclass = class &lt;&lt; included_class; self; end metaclass.send :define_method, :inherited do |inherited_class| trace_class_creation do #Puxa todos os métodos da classe cujo este módulo foi incluído (classe abstrata) abstract_methods = included_class.public_instance_methods(false) abstract_methods += included_class.private_instance_methods(false) abstract_methods += included_class.protected_instance_methods(false) <pre><code> #Puxa todos os métodos da classe que herdou da classe abstrata inherited_methods = inherited_class.public_instance_methods(false) inherited_methods += inherited_class.private_instance_methods(false) inherited_methods += inherited_class.protected_instance_methods(false) #Para cada método na classe abstrata, ver se ele está incluso na classe herdada. abstract_methods.each do |m| #Se não estiver incluso, lançar uma exception. raise NoMethodError, &amp;quot;Method #{m} not implemented.&amp;quot; unless inherited_methods.include?(m) end end end </code></pre> end end
Bom, basicamente, é isso. Agora, para fazer o teste, basta salvar todo esse código maluco em um arquivo, digamos, “abstract.rb” e usar da seguinte forma:
require 'abstract' class AbstractClass include Abstract def a_method end end class Inherited < AbstractClass end #Vai lançar uma exception, aqui. class Inherited2 < AbstractClass def a_method end end #Nenhuma Exception
Note que, mesmo no primeiro exemplo, a classe É criada, apesar da exception (claro que você só conseguirá ver isso se usar o IRB ou capturar a exception). Uma alternativa é remover a constante, usando o método privado Object#remove_const. Vou atualizar o meu GitHub com essa idéia, assim que eu tiver um pouco de paciência para fazer.
Ufa, chega por hoje. Achei interessante, mais uma vez, que mesmo fazendo coisas que são obviamente contra tudo o que a linguagem Ruby se propõe a fazer, ainda assim a solução é bem limpa. No próximo, estou pensando em armar algo com DRB, vamos ver o que sai!