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 &quot;module&quot;, assim tanto o &quot;extend_me&quot; #quanto o &quot;instance_extend&quot; ficarão disponíveis para classes e módulos. class Module def extend_me #aqui, eu optei por usar o diretório &quot;extensions&quot;. Repare que não é &quot;app/extensions&quot;. file_name = Rails.root.join(&quot;extensions&quot;, &quot;#{name.underscore}.rb&quot;) #pela forma como o Rails funciona, se estivermos em ambiente de dev, o ideal é mudar essa #linha para &quot;load&quot;, 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(&amp;block) #criamos um module anônimo com o bloco como parâmetro mod = Module.new &amp;block <pre><code>#acredito que no Rails 3, esse método &amp;quot;metaclass&amp;quot; chama-se &amp;quot;singleton_class&amp;quot;. #a idéia aqui é redefinir o método &amp;quot;new&amp;quot; da classe que está sendo extendida, ou #seja, sempre que fizermos Monkey.new... metaclass.send :define_method, :new do |*args, &amp;amp;b| #chamaremos o construtor padrão do Monkey... object = super(*args, &amp;amp;b) #faremos &amp;quot;extend&amp;quot; no objeto retornado pelo construtor padrão... object.extend mod #e retornaremos o objeto extendido já. object end </code></pre> 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.