1. Computing

Refinements: Monkeypatching Made Safe

By

This article is about a new feature in Ruby 2.0. To read more about Ruby 2.0, go to the main article on Ruby 2.0 new features.

Feel free to patch monkeys to your heart's content, because as of Ruby 2.0 it can be done in a safe manner. Refinements are a way to implement monkeypatches in a sort of module that can be activated in scopes where the behavior is intended, but leaves all other scopes unchanged. Monkeypatching has always been a risky prospect, controversial and, to some, considered a hack or too risky to do. But with this new feature, it's a perfectly sane and reasonable thing to do.

A monkeypatch is an alteration to a class' behavior, usually a base class like String, to change its behavior in some way. There are a number of reasons why you'd want to do this. You can do this to make your code more expressive. For example, ActiveSupport enables expressions like 10.seconds.ago, patching both the Fixnum and DateTime classes to implement the methods seconds and ago. This is too expressive to ignore, even if you do consider it a hack.

You can also monkeypatch a class to fix existing code. Say your code relies on a particular method that's been removed or changed. An easy way to fix this is to redefine the method so it exhibits its old behavior. Your code won't know the difference, and where you had to do many fixes before, you can do just one now. Or another reason would be to modify the behavior of your code. Say you hard-coded a number of puts statements (really a call to the Kernel::puts method) but you now need to redirect those to a log file. Simple, redefine the method to write to the log file instead.

But no matter the reason for the monkeypatch, there's always the risk of blowback, unintended consequences of your redefinitions of methods in classes used in code beyond your control. Maybe a gem you use uses that method behind the scenes, and you don't know about it. Or even your own code, it's hard to see all interactions any change will have. Because of your change your program suddenly stops working several method calls deep inside a gem you know should work. This is the danger of monkeypatching, this is why without refinements it was done very carefully, if at all, and more often than not adding new methods instead of redefining old methods.

Refinements

Refinements are a way to limit the scope of your monkeypatches. Think of them like as something like a mixin module. You can include the mixin at the class level, imbuing all instances of that class with this new behavior. Or, you can extend specific objects with this mixin module, limiting the scope. A refinement is like this in concept, but implemented a bit differently.

Refinements are defined using the refine method. It takes a single argument, the class you want to change the behavior of, as well as a block. Inside this block you're going to define the methods you want to change the behavior of. And all of this goes inside a module so the refinement has a name.

Once you're in the scope you need to use the monkeypatch (or "refinement," if you insist on calling it that), call the using method with the module holding the monkeypatch (or monkeypatches, they can hold more than one) you want to activate. At this scope, or any deeper scope, the refinement will be active.

In the following example, we'll implement

10.seconds.ago, implementing both the Fixnum#seconds and DateTime#ago methods. Since these will obviously conflict with those from ActiveSupport, we'll hide them away in a refinement.


#!/usr/bin/env ruby

module SecondsAgo
  refine Fixnum do
    def seconds
      self  # Durations are seconds, so just self
    end

    def minutes
      self * 60
    end

    def hours
      self * 60 * 60
    end

    def ago
      Time.now - self
    end
  end
end

# Check that the monkeypatch is off
begin
  puts 10.seconds.ago
rescue NoMethodError
  puts "Good, off by default"
end

class Tester
  # Check that the monkeypatch is on
  using SecondsAgo

  def do_test
    puts 10.seconds.ago
    puts 10.minutes.ago
  end
end

# Check that the monkeypatch is still off
begin
  puts 10.seconds.ago
rescue NoMethodError
  puts "Good, still off in file scope"
end

Tester.new.do_test

As you can see, this is all rather straightforward. The monkeypatch is defined in the SecondsAgo module (even though it also defines minutes and hours, perhaps not the best name). When, inside the Tester class, we want to use these monkeypatched methods, we use the using method to activate the monkeypatch (OK, OK, "refinement," if you must). In this scope, and in all scopes deeper than it, including the method defined just below it, the monkeypatch will be active. But back in the file scope, the monkeypatch is still off. Success!

  1. About.com
  2. Computing
  3. Ruby
  4. Advanced Ruby
  5. Refinements: Monkeypatching Made Safe

©2014 About.com. All rights reserved.