Wednesday, December 19, 2012

Reopen and Modify Ruby Classes (Monkey Patching)

As programmers, we often reuse code other programmers have written. It makes no sense to duplicate code if there are already libraries and/or modules available to us. Most of the time, we don't care about the quality of the code as long as it behaves as we expect. After all, who wants to know what the sausage factory looks like?

What if we want to modify how the sausage is made? Well, because Ruby classes are open, we can add/modify methods in existing pieces of code. We simply reopen the class and define (or redefine) the methods. This is often known as monkey patching. I'm not going to preach about how great or evil this "technique" is, but if you're interested, check out this article by Jeff Atwood.

Classes We Cannot Touch

Let's say I am using a class written by a friend called FriendClass. We'll assume it has two methods I'm allowed to use:
  • my_greeting()
  • my_goodbye()

Here is the usage:
f = FriendClass.new

f.my_greeting("Fred")
# Hello, Fred.

f.my_goodbye
# Goodbye!

Reopen and Add a Method

Assume I don't have access to the implementation of the class. What if I want to add a new method to it? We just reopen the class:
class FriendClass

  def my_insult
    puts "You suck."
  end

end

f = FriendClass.new

f.my_insult
# You suck.
It might look like we've completely redefined the class, but Ruby knows that FriendClass has already been defined. It simply adds the new method to the existing definition. So, I can still use the methods originally defined by my friend as well:
f.my_insult
# You suck.

f.my_goodbye
# Goodbye!

Reopen and Modify a Method

Now, I want to change the boring greeting in FriendClass. Just like adding new methods, we can completely change the behavior of existing methods using the same technique:
f = FriendClass.new

f.my_greeting("Fred")
# Hello, Fred.

class FriendClass

  def my_greeting(name)
    puts "Hey there, #{name}! How's it going?"
  end

end

f.my_greeting("Fred")
# Hey there, Fred! How's it going?
Voila! The class now has the behavior I want without changing the actual implementation of FriendClass.

Reopen and Add to Any Class

This type of modification can be done to any class in Ruby. This includes Gems and core Ruby classes. Let's say I wanted to be able to (naively) sum elements in my Arrays:
[1,2,3,4].sum
# undefined method `sum' for [1, 2, 3, 4]:Array (NoMethodError)

class Array

  def sum
    sum = 0
    self.each do |e|
      sum += e
    end
    sum
  end

end

[1,2,3,4].sum
# 10

Monkey Patch a Ruby Class

If you want to modify the behavior of a method, but retain some of the existing functionality, there are several ways to do this. Each have pros and cons. The most popular way is to use alias:
f = FriendClass.new

f.my_greeting("Fred")
# Hello, Fred.

class FriendClass

  alias old_my_greeting my_greeting
  def my_greeting(name)
    puts "I used to say '#{old_my_greeting(name)}'. 
    puts "Now I say, what's up, #{name}?"
  end

end

f.my_greeting("Fred")
# I used to say 'Hello, Fred.'. 
# Now I say, what's up, Fred?
By using alias, we can safely redefine my_greeting and still have access to the original version through old_my_greeting.

Alternative Solutions for Gems

Occasionally, you will be tempted to monkey patch a buggy Gem. Before doing so, it is important to consider alternative solutions. My favorite alternative to monkey patching is forking the Gem on Github, making a change, and adding the url to the Gemfile.

Another solution is to directly modify the Gem code in your local environment. By using gemedit, you can quickly modify any Gem in your preferred editor. You can also use gem unpack to create a private copy of a Gem's contents in your current directory. Only do this if portability is not important for you project.

Please leave your corrections, suggestions, and opinions on monkey patching in the comments below!

3 comments: