O Método “Extend” e Seus Usos

Provavelmente muita gente conhece o método “extend”, usado principalmente em classes para adicionar métodos, tais como:

module Nameable
  def set_name(name)
    @name = name
  end
end

class MyClass
  extend Nameable
  set_name "Foo Bar"
end

Claro que há pessoas que fazem verdadeiras aberrações, tipo um “module” que define o callback “included” que chama um “extend”, tipo essa situação:

module Nameable
  def self.included(klass)
    klass.extend Nameable::ClassMethods
  end

  module ClassMethods
    def set_name(name)
      @name = name
    end
  end
end

class MyClass
  include Nameable
  set_name "Foo Bar"
end

Mas vamos ignorar esse tipo de coisa e pensar em outras formas de usar o “extend”. Digamos que temos uma classe como a seguir:

class Authenticator
  def login(username, password)
    if User.find_by_username_and_password(username, password)
      return true
    else
      return false
    end
  end

Ok, temos uma regra para autenticar (aviso: não use isso em produção, o código prevê que os usuários tem suas senhas gravadas no banco sem criptografia nenhuma). Digamos, agora, que em um determinado cliente, esse código só não é o suficiente: o cliente quer que, antes de autenticar no banco, se autentique no sistema

Uma solução é usar monkey-patch. Nesse caso, teríamos um código em outro lugar que redefiniria a classe e adicionaria novos métodos, tipo:

class Authenticator
  def login(username, password)
    return true if ThirdPartySystem.authenticate(username, password)
    if User.find_by_username_and_password(username, password)
      return true
    else
      return false
    end
  end
end

Percebe-se que o código de autenticação ficou duplicado. Para resolver esse caso, podemos usar “extend” de uma maneira bem interessante:

module ClientAuthenticator
  def login(username, password)
    return true if ThirdPartySystem.authenticate(username, password)
    super
  end
end

authenticator = Authenticator.new
authenticator.extend ClientAuthenticator
authenticator.login("admin", "blabla")

Ou seja, o “extend” pode ser usado para adicionar funcionalidades à instâncias também. O exemplo acima, de certa forma, é o design pattern “decorator” (do GoF), adaptado para Ruby. Porém, podem existir casos aonde o que se deseja é extender a funcionalidade automaticamente. Digamos que estamos programando um aplicativo aonde determinados controllers acessam determinados models. Digamos que um controller SEMPRE vai instanciar um model “Person”, mas dependendo do cliente aonde instalaremos esse aplicativo, o model terá customizações diferentes. Uma das coisas que podemos fazer é criar um module que automaticamente irá ser extendido pelo aplicativo, por exemplo:

class Monkey
  def name
    "I'm a Monkey!"
  end

  extend_me
end

Claro que esse método “extend_me” deve ser implementado em algum lugar. A idéia é que o método “extend_me” vai fazer “require” de algum código, e esse código informará o que deve ser extendido. Como uma API para isso, pensei em algo assim:

#arquivo extensions/monkey.rb (por exemplo)
Monkey.instance_extend do
  def name
    old_name = super
    "#{old_name}-Patch"
  end
end

Bom, para isto funcionar, precisamos agora definir os métodos “instance_extend” e “extend_me”. Digamos, no “lib/instance_extend.rb”, podemos escrever os códigos:

#Redefinindo os métodos da classe "module", assim tanto o "extend_me"
#quanto o "instance_extend" ficarão disponíveis para classes e módulos.
class Module
  def extend_me
    #aqui, eu optei por usar o diretório "extensions". Repare que não é "app/extensions".
    file_name = Rails.root.join("extensions", "#{name.underscore}.rb")
    #pela forma como o Rails funciona, se estivermos em ambiente de dev, o ideal é mudar essa
    #linha para "load", para carregar cada vez que recarregarmos a página.
    require(file_name) if File.exists?(file_name)
  end

  #a mágica vem aqui: instance_extend recebe um bloco...
  def instance_extend(&block)
    #criamos um module anônimo com o bloco como parâmetro
    mod = Module.new &block

    #acredito que no Rails 3, esse método "metaclass" chama-se "singleton_class".
    #a idéia aqui é redefinir o método "new" da classe que está sendo extendida, ou
    #seja, sempre que fizermos Monkey.new...
    metaclass.send :define_method, :new do |*args, &b|
      #chamaremos o construtor padrão do Monkey...
      object = super(*args, &b)
      #faremos "extend" no objeto retornado pelo construtor padrão...
      object.extend mod
      #e retornaremos o objeto extendido já.
      object
    end
  end
end

Para testar esse código, basta em algum lugar rodar “Monkey.new.name”. O retorno deverá ser “I’m a Monkey-Patch”. Porém, se for necessário testar o comportamento padrão, basta remover a linha “extend_me” e rodar “Monkey.new.name” de novo. O retorno será o padrão “I’m a Monkey”.

Sobre performance… nos meus testes, instanciar essa classe é cerca de 2x mais lento do que o código sem o “extend_me”, justamente por causa do overhead do “extend”. Porém, a partir da classe instanciada, a diferença de performance de usar isso ou usar, por exemplo, instâncias, é equivalente.

Advertisements
This entry was posted in 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