This post is about limiting the scope of “monkey patches” with Ruby’s Refinements. This is certainly nothing new - there are numerous blogs posts and talks on this topic if you just Google around a bit. Despite Refinements having been around in Ruby since version 2.0, it’s not something we see very often. For that reason, it can help revist the topic in order to get re-acquainted with it, or learn something new.
The Problem with Ruby’s Open Classes
As usual, to understand a thing, we need to understand the problem that thing is meant to solve.
The Open Class technique, more commonly known as ‘monkey patching’, is a meta programming technique that lets the developer add new methods (or change existing ones) to a class at run-time. For example:
In this example, the
String class is re-opened and the
randomize method is added.
Doing this affects every existing instance of
String and all new instances going forward.
This works because instance methods are stored in the class object.
So, what’s the problem with this?
“Monkey patching” is global. The above change affects every
String object in the
entire application. Some things to consider:
- Will it conflict with a 3rd party library?
- Could we accidentally override an existing method?
- Will our patch remain compatible with future versions of Ruby?
We can limit the scope of our “monkey patches” by calling
refine inside a module
A Refinement is not active just by defining it. To activate a Refinement, it must be done
To activate Refinement call
We can do this inside a module or class so that our patch is only active inside a module or class definition.
A Refinement is active is two places:
- Inside the
- Starting at the place in the code where
usingwas called until the end of the definition if inside a module or class.
- Methods already called in a definition are not Refined after calling
using. Here is an example:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 class Calculate def add1_to(num) num + one end def one 1 end end module CalculateExtensions refine Calculate do def one 1.0 end end end using CalculateExtensions Calculate.new.one #=> 1.0 Calculate.new.add1_to(2) #=> 3
After we call
using, it’s reasonable that one would expect
Calculate.new.add1_to(2) to return
due to coercion, but we’re actually still adding
1 and not
1.0. That’s because the call to
add1_to(num) method on line 3 happens before the call to
using on line 19.
usingdirectly in IRB at the moment doesn’t work. You can learn more about this here.
Refinements are an easy way to limit the scope of our “monkey patches”, thereby making them much safer to implement. We can avoid unexpected results that can come with making global changes to our code, yet still take advantage of Open Classes.