(ruby, monkey-patching, refinements, metaprogram all the things)
Monkey-patching. It strikes fear into newcomers to Ruby, excitement and abandon in moderate Ruby developers, and sad, disgruntled sighs from the experienced developers who end up having to wade through a huge Rails codebase to find out where somebody decided to monkey-patch the
mysql2 gem in a way that makes it incompatible with a Rails 4 upgrade (bonus points for hiding the patch in something like
(bitter tears of experience here)
But hark! Ruby 2.1 is here, and may contain some salvation in the form of refinements. First, though, a refresher.
Patch that Monkey!
Monkey-patching in Ruby involves modifying code at run-time, either by extending and providing additional functionality or redefining existing code. And Ruby makes it so easy! Consider this little gem:
‘❤’ * self.length
This extends String with the method
This is how large chunks of the Rails helper methods (e.g.
:camelize) are implemented. And this is fine…except due to the Magic of Ruby, you can do this pretty much anywhere in your code. And you can do this as well:
Just to give you an idea of the systemic breakdown this can create, let’s go back to our earlier method:
Again, there are sometimes reasons why you have to monkey-patch and replace methods at runtime. Perhaps your database connection has some legacy weirdness that needs to be worked around and it isn’t covered by a standard gem (this is a code smell, obviously, but sometimes it has to be done). Being able to do this at-will in a large code-base, however, can easily lead to problems. Maybe you do your patch in an obscure area of the code, and a new developer writes a method that uses the expected behaviour, and then spends a day or so trying to work out why her code isn’t working correctly. Or even worse, somebody decides to write another monkey-patch on the same method, meaning that the behaviour in production becomes a Fun Race Condition That Wakes You Up At 3am depending on which one is executed by Ruby first.
The real answer is Don’t Monkey-Patch, and enforce it with hammers. But sometimes it’s not possible to avoid that monkey. Thankfully, Ruby 2 delivers a way of at least limiting the scope of patches with Refinements.
refine String do
'❤' * self.length
Module#refine takes a class and a block, and in that block, you can extend or redefine to your heart’s content. But:
NoMethodError: undefined method `all_is_love’
Refinements need to be specifically activated with the
Okay, so far, much the same as normal monkey-patching. But the magic is that the patch only exists in that scope. So if we embed them in a Class, the refinement only exists within that class. For example:
=> NoMethodError: undefined method `all_is_love’
The monkey-patch is restricted to the class where it is activated. As you can imagine, this is all sorts of useful - not only do you not pollute the global scope with your redefinitions or extensions, but you also make it explicit that you’re using that particular patch (and also providing a giant big clue where the patch is defined).
So, still, don’t go crazy with your monkey-patching, but, if you have to use them (and you’re running on a current Ruby 2.1.x version), consider using refinements as a way to limit their problematic features…
(however, do be aware that there are a few odd things that may bite you if you’re not paying attention. In particular:
refine String do
HEART_LENGTH = 1
'❤' * HEART_LENGTH
Constants and class variables within a refinement belong to the module, not the class being refined:
NameError: uninitialized constant
So be aware of that when adding your refinements)