Go to English Blog

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.