Usando Refinements no Ruby 2.1
Leia em 2 minutos
Refinements foi adicionado no Ruby 2.0, mas se mostrou muito controverso, então seu escopo foi reduzido e a funcionalidade marcada como experimental. Mas com o lançamento da versão 2.1, refinements veio para ficar!
A ideia do refinements é permitir um maior controle sobre o processo de Monkey Patching, tornando-o mais seguro. Com ele é possível estender classes em um contexto específico. Para usar refinements, basta definir um módulo que estende as classes que você precisa; para isso, utilize o método Module#refine
.
module TimeExtensions
refine Integer do
def minutes
self * 60
end
end
end
A definição do refinement não faz nada por si só; se você inspecionar a classe Integer
verá que o método não foi definido.
2.minutes
#=> undefined method `minutes' for 5:Fixnum (NoMethodError)
Para carregar este refinement em um contexto específico é preciso utilizar o método Module#using
.
class MyClass
using TimeExtensions
puts 5.minutes
#=> 300
end
Note que apenas aquele contexto irá definir o método Integer#minutes
. Mesmo em uma subclasse que herda de uma classe que faz uso de um refinement, é preciso chamar o método Module#using
novamente. Isso acontece porque o refinement só é ativado em um dado contexto léxico.
class OtherClass < MyClass
puts 5.minutes
#=> undefined method `minutes' for 5:Fixnum (NoMethodError)
end
Mas quando o super
é invocado, o processo de method lookup inspecionará a superclasse e, neste caso, o método definido pelo refinement será utilizado.
module TimeExtensions
refine Integer do
def minutes
self * 60
end
end
end
class Foo
using TimeExtensions
def initialize
puts 5.minutes
end
end
class Bar < Foo
def initialize
super
end
end
Bar.new
#=> 300
Para carregar um refinement globalmente, basta chamar o método Kernel#using
. Mas atenção! O escopo global de um refinement é aplicado somente àquele arquivo; outros arquivos precisarão fazer a sua própria chamada!
# time_extensions.rb
module TimeExtensions
refine Integer do
def minutes
self * 60
end
end
end
require_relative "a"
require_relative "b"
# a.rb
using TimeExtensions
puts 5.minutes
#=> 300
# b.rb
5.minutes
#=> undefined method `minutes' for 5:Fixnum (NoMethodError)
Uma coisa muito ruim de usar refinements é que acesso indireto, como os métodos Kernel#send
e Kernel#respond_to?
, são ignorados.
using TimeExtensions
5.minutes
#=> 300
5.respond_to?(:minutes)
#=> false
5.send(:minutes)
#=> undefined method `minutes' for 5:Fixnum (NoMethodError)
5.method(:minutes)
#=> undefined method `minutes' for class `Fixnum' (NameError)
Segundo a documentação, este comportamento pode mudar em versões futuras.
Finalizando
Open Classes nunca foi um problema real na comunidade Ruby. Em quase 8 anos que trabalho com Ruby, só fui surpreendido por um monkey patch mal-feito apenas uma vez (o desenvolvedor não respeitou a aridade do método).
A implementação do refinements pode parecer uma boa ideia, mas ainda tem muitas incertezas. Ainda acho arriscado usar essa funcionalidade e, por enquanto, vou ficar longe.