Dá para criar classes abstratas em Ruby?

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 << 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)

        #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, "Method #{m} not implemented." unless inherited_methods.include?(m)
        end
      end
    end
  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!

Advertisements
This entry was posted in Coisas que você nunca quis fazer com Ruby, 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