1 min read

Clean Monkey Patching in Ruby

I have a library and I want to change some of its behaviours. I could straighout monkey patch, or I could be a lot cleaner
Clean Monkey Patching in Ruby
Photo by Leila Boujnane / Unsplash

I have a library and I want to change some of its behaviours. I could straighout monkey patch, or I could be a lot cleaner.

Ruby comes with so many tools to modify an existing class (and to shatter the Open/Closed Principle) but it also comes with a rather beautiful way to change the class hierarchy: prepend.

In my case, I wanted to change the behaviour of one method in a library's class but still have access to original implementation. I wanted to be able to call super.

Let's take this code:

module Plant
end

class Beanstalk
  include Plant
  
  def water
    'grow'
  end
end

So I have a little Beanstalk and if I water one, it'll answer with 'grow'. Let's have a look at how ruby has built up the inheritance chain:

Beanstalk.ancestors
# => [Beanstalk, Plant, Object, Kernel, BasicObject]

That's fine, but I want to change the behaviour of the water method and it's in some library file or other. I could just monkey-patch and reopen the class and overwrite the method with my implementation but that just blasts away the previous version of that method and I want something more subtle.

This is where prepend comes in. In my head, I couldn't quite see what prepend was for: I imagined it prepending something to the inheritance tree before the class or module I was currently defining (which is exactly what include does). But this is where a look at the results from ancestors really helps.

Let's try this:

module GiantBeanstalk
  def water
    "#{super} very, very tall"
  end
end

Beanstalk.prepend(GiantBeanstalk)

Now let's look at the inheritance tree:

Beanstalk.ancestors
# => [GiantBeanstalk, Beanstalk, Plant, Object, Kernel, BasicObject]

Ho! GiantBeanstalk has been prepended to the ancestor chain. If I ask for a Beanstalk.new, I'm actually going to get a GiantBeanstalk and one that can call super on the ancestor methods...

Beanstalk.new.water
# => "grow very, very tall"