January 20, 2015 by Daniel P. Clark

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"

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" 

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" 

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

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

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.

#alias#article#block#blog#object#post#refine#refinement#ruby#scope

Comments

  1. ferdinandrosario
    January 22, 2015 - 1:57 am

    Nice Article. you helped me to be clear with refinement .Thanks

    • Daniel P. Clark
      January 23, 2015 - 12:05 am

      Glad I could help! Feel free to ask any questions you may have.

  2. dimakura
    September 8, 2015 - 4:10 pm

    Hi, Daniel!

    Thanks for writing this article.

    It’s really interesting feature added in Ruby2. And I can imagine several use cases for using it.

    Still it looks a bit restricted. I wanted to ask your opinion about how usable Refinements might be in replacing monkey patches for Rails core classes.

    As an illustration what I mean. I just found a question on StackOverflow and was thinking about using Refinements in it (instead of monkey patch) http://stackoverflow.com/a/32457119/1426097. It basically extends functionality of ActiveRecord::LoggerSubscriber class. While it should be used internally by Rails itself, I can’t figure out how Refinements might be useful in this context.

    • Daniel P. Clark
      September 11, 2015 - 8:38 am

      I’ve written a follow up post on refinements: Ruby Refinements – Not quite ready for production

      Refinements are virtually invisible to any outside introspection which can lead to troubles with testing and verifying what’s going on under the hood. There are “planned changes” to eventually allow introspection, but for now I’d be careful about using it. Don’t mix refinements (for now) and use them in a very clear way as to not need the use of introspection.

      Sandi Metz, the author of several excellent Ruby books, has done a conference talk called “Nothing is Something”. Towards the end of that talk she gets into proper Object design and dealing with refining an Object by determining ideal attribute objects. I recommend it before you continue.

      https://www.youtube.com/watch?v=9lv2lBq6x4A

      In your StackOverflow question example it’s important to note that the link is to a module. Refinements can only be included within a class. It’s written as a module and used in a class. The keyword “using” is only available within that scope.

      • dimakura
        September 14, 2015 - 3:48 am

        Thanks, Daniel!

Leave a Reply

Your email address will not be published / Required fields are marked *