Refinements over Monkey-patching

Monkey patching is rather straight forward. You take an existing object and you apply your own duct tape, glue, nuts and bolts, or even chewing gum. Or if it’s bad you hit it with a hammer. No, but more seriously, it’s when you modify something existing from outside it’s original project code.

For example you can add a method to the String Object by monkey patching like so:

"asdfghjkl".at 5 # NoMethodError: undefined method `at' for "asdfghjkl":String class String def at(num) self[num..num] end end "asdfghjkl".at 5 # => "h" 1 2 3 4 5 6 7 8 9 10 11 12 "asdfghjkl" . at 5 # NoMethodError: undefined method `at' for "asdfghjkl":String class String def at ( num ) self [ num .. num ] end end "asdfghjkl" . at 5 # => "h"

As you can see the method .at() does not currently exist as an available method on String. But when we defined the method within the class String it became available.

If you’re new to monkey patching then I should inform you that the String Object is not redefined or rewritten here… but it has been built further upon. So when you use monkey patching to define a new method, all the other methods and functionality still remains within the class. But if you were to redefine an existing method it “does overwrite it”.

"asdf".capitalize # => "Asdf" class String def capitalize self.replace self.reverse end end "asdf".capitalize # => "fdsa" 1 2 3 4 5 6 7 8 9 10 11 12 "asdf" . capitalize # => "Asdf" class String def capitalize self . replace self . reverse end end "asdf" . capitalize # => "fdsa"

For this reason you want to be very cautious against doing anything potentially dangerous with existing code. The main reason is “when you monkey patch: it changes the way that Object works EVERYWHERE“. Also if you still want to maintain the previous behavior you’ll need to keep the old method around with alias_method.

class String alias_method :old_capitalize, :capitalize def capitalize self.replace self.reverse self.old_capitalize end end "asdf".capitalize # => "Fdsa" 1 2 3 4 5 6 7 8 9 10 11 class String alias_method : old_capitalize , : capitalize def capitalize self . replace self . reverse self . old _ capitalize end end "asdf" . capitalize # => "Fdsa"

So now we’ve added in the new behavior we wanted to to capitalize and still called the old one. We still kept it’s original function around be renaming it with alias_method. The old method_missing is the new method named old_method_missing.

Keep in mind that this change, and/or behavior, is now happening everywhere. It’s wreaking havoc across the universe with unforetold damages (tongue-in-cheek). If you want proof; try adding the method each to the String Object in a Rails project. It’ll blow up within the Cookies library. Or you can try adding the flatten method into String… this will disable Rubygems… all of them… you won’t be able to import any. At least that’s what happened back when I tried it… Rubygems may have changed enough since then for that to not be the case.

There are plenty of times where the monkey-patching way is exactly what you want to use. Like by adding logging to the nil class to help you know when your code unexpectedly calls an Object that wasn’t supposed to be nil:

class NilClass alias_method :old_method_missing, :method_missing def method_missing(m, *args, &block) Rails.logger.debug "#{m} called on nil object with #{args} attributes#{ ' from ' + block.try(:source_location).to_s if block}." old_method_missing(m, args, block) end end nil.cow # cow called on nil object with [] attributes. # NoMethodError: undefined method `cow' for nil:NilClass 1 2 3 4 5 6 7 8 9 10 11 12 13 14 class NilClass alias_method : old_method_missing , : method _ missing def method_missing ( m , * args , &block ) Rails . logger . debug "#{m} called on nil object with #{args} attributes#{ ' from ' + block.try(:source_location).to_s if block}." old_method_missing ( m , args , block ) end end nil . cow # cow called on nil object with [] attributes. # NoMethodError: undefined method `cow' for nil:NilClass

One thing to consider: if your project includes lots of Gems or code from other people, they may have also re-aliased methods. So one person could be undoing the other persons work if you re-alias with the same naming. So if you working on a Gem project, or the like, you might consider aliasing more like alias_method :old_method_missing_in_myproject, :method_missing but try to keep the name a little bit shorter then that. This will help prevent against collision with other projects modifications.

Refinements

Refinements are the same thing as monkey patching, except without everything blowing up. Refinements are your monkey patches getting used only within the scope that you call it to be used. For example:

module MyStringThing refine String do def at(num) self[num..num] end end end class A using MyStringThing def fifth(str) str.at(4) end end class B def fifth(str) str.at(4) end end A.new.fifth("qwerty") # => "t" B.new.fifth("qwerty") # NoMethodError: undefined method `at' for "qwerty":String 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 module MyStringThing refine String do def at ( num ) self [ num .. num ] end end end class A using MyStringThing def fifth ( str ) str . at ( 4 ) end end class B def fifth ( str ) str . at ( 4 ) end end A . new . fifth ( "qwerty" ) # => "t" B . new . fifth ( "qwerty" ) # NoMethodError: undefined method `at' for "qwerty":String

Here you see that the method .at() is defined as a refinement within the module MyStringThing. And only in class A did it work. That’s because the using MyStringThing brought in the refinement within the scope of the class A. Now with refinements we can define each, or flatten, on String and not blow everything up! Because we’ll be scoping it to only where we need it. Note: a refinement is defined within a module and not a class.

So refinements allow you to put your Mad Scientist ideas to the test but restricts your explosions to just the lab you’re working in instead of you obliterating the whole world. Now people don’t mind your insanity that much since you no longer appear to be a threat. Yes, they don’t even mind that you’ve given the monkey a lab coat and set him loose. “As longs as you’ve scoped him” we’re all “safe” ;-).

I must add that monkey patching isn’t generally as dangerous as I may have joked about. Just keep in mind that there may be some cause and effect. Don’t fear monkey patching. Just use it wisely when you need to. And if you only need it within a certain “area”, as opposed to everywhere, then use refinements.

As always I hope my writing was both informational and enjoyable. Please comment, share, subscribe to my RSS Feed, and follow me on twitter @6ftdan!

God Bless!

-Daniel P. Clark

Image by Dave Stokes via the Creative Commons Attribution 2.0 Generic License.