Make Monkey Patching in Ruby Less Risky with Refinements

Ruby makes it easy to extend its built-in classes, which can be very convenient and lead to more readable code—but it can also be dangerous. This practice, known as “monkey patching,” is common in the Ruby world, and since Ruby 2, it’s been possible to mitigate some of the risks using refinements.

As beneficial as refinements are, they haven’t been embraced as warmly as one might expect. Part of this might have to do with the fact that Rails, home to some of the most extensive monkey patching in the Ruby community, has been slow to adopt them.

Monkey Patching – The Old Way

Monkey patching is useful when you find yourself wishing an existing class had some useful piece of functionality that wasn’t originally built into it. One useful function that Ruby’s String class doesn’t provide by default is ‘titleize’, which takes a string and capitalizes the first letter of each word.

It’s easy enough to make a function that will titleize a string, then put it in a module with some helpers and import it when you want to use it, but that isn’t very OO. If you open the class and add the method to the class definition itself, it’s as easy as calling the method on any string.


"moby dick".titleize # -> Moby Dick

In Ruby, you can open a class at any time by writing a class definition.


class String
  def titleize
    # your implementation here
  end
end

If the class you name doesn’t exist, it is created. If it does exist, it is opened. When it’s open, any definitions you make inside will be available to all instances of the class. If the definition you make inside already exists on the class, the old definition will be wiped out and replaced with yours.

Pitfalls

If you’ve always used Ruby with Rails, you might be thinking that titleize already exists in the String class. That’s because Rails makes it available. However, if you’ve never used Ruby with Rails, you might not have even known that titleize is a thing. This is the first problem with monkey patching; everyone has inconsistent ideas about what’s included with the standard library.

Worse than this, though, is the risk of accidentally overriding a method that already exists. It can lead to mysterious errors in other parts of your codebase, or even in libraries.

Maybe your application needs a titleize function to randomly slap titles onto the beginning and end of a string:


"Judi Dench".titleize # "Dame Judi Dench CH, DBE, FRSA"

When you plug your code into a Rails application, things suddenly don’t work as intended. Other libraries start breaking, and you start seeing titles appear in other parts of your application.


"moby dick".titleize # "Sir Moby Dick Esq. KBE"

Even if you’re positive that the method name you’re using is unique, you can still run into issues. Maybe you monkey-patch a unique method into a class and then later, you add a library that monkey-patches the same method into the same class. The implementation might differ enough that it breaks your existing code.

Enter Refinements

With refinements, it’s possible to monkey-patch existing classes without polluting the class globally. Your refinement will live in a module that you will use in places where you want the modified behavior.


module M
  refine String do
    def titleize
      # Implementation
    end
  end
end

Then, in your code, you just need to write `using M`. In the scope of that `using` statement, any calls to titleize will use your custom version. Outside the scope of `using`, it will use the original version.

Monkey-patching this way allows you to opt into custom behavior without having it leak out into other parts of your application. Even though the Ruby community as a whole has been slow to adopt this method, it’s worth working into your own code when you find yourself wanting to monkey-patch an existing class.

Conversation
  • oldfartdeveloper says:

    I’ve read three blog posts on this topic; yours was the clearest and most succinct. I learned that I had assumed that monkey-patching worked like refinements; very glad to learn I was wrong. Thank you.

  • Comments are closed.