Monkey Patching in Ruby - Real World Example

Mon 09 Oct 2017
Reading time: (~ mins)

"Monkey patching is like violence. If it's not working, you aren't using enough of it." - some popular Rubyist

One of the fun things(read: absolutely terrifying) in ruby is learning about the ability to override any class anytime you want. With great power comes great responsibility. Any piece of code you run can cause major damage or unexpected results if somewhere along the way someone put in something nasty like:

class String
  def empty?
    true
  end
end

Now imagine overriding a more important class? Scary right! Against the general rule of thumb, sometimes we forgo the warnings of other rubyists and go ahead to patch in stuff to allow for nicer DSLs and APIs. Here is a real example that I came across while working on the Surrealist gem.

The problem faced was that our method #surrealize worked only on single records returned from ORMs(like ActiveRecord). When dealing with collection of records it is harder for us to hook in and provide a custom method.

User.create(name: 'Alessandro')
User.first.surrealize # => "{\"name\":\"Alessandro\"}" 
User.all.surrealize   # => NoMethodError

Most ORMs will pass us back an array of objects or in the case of ActiveRecord, some sort of dynamically built class. So it seemed pretty straightforward, if we want to continue using this same API to surrealize collection of records we need to start monkey patching.

# implementation simplified	
class Array
  def surrealize
    map do |record|
      Surrealist.surrealize(record)
    end
  end
end

# Now using Sequel ORM
User.insert(name: 'Alessandro')
User.first.surrealize # => "{\"name\":\"Alessandro\"}"
User.all.surrealize   # => ["{\"name\":\"Alessandro\"}"]

So this works and from a user's point of view it seems like expected behaviour. The problem though is that they are unaware that I'm in and mucking around with Array. To get ActiveRecord working as well, some custom violence is needed:

if defined?(ActiveRecord)
  module ActiveRecord::Delegation
    delegate :surrealize, to: :records
  end
end

ActiveRecord returns a dynamic instance, ActiveRecord_Relation, built from the model. We just delegate our method to #records which actually produces an Array holding all the records(duh). Since we already patched Array this now works!

Now the question is, is this the right way to solve the problem? That's a tough call.

- Will more `things` probably need custom patching?
- Potentially breaking stuff due to unexpected behaviour in trusted classes?
- Is there another way which does not require patching?
- Is creating a new interface okay?
If you can confidently answer yes to all of these then maybe you need to take a step back from the keyboard and stop monkey-patching. Despite it being fun to see what you can get away with, this kind of code can come back and bite...silently.

The way it was solved on Surrealist was ultimately not going down the path of monkey-patching. Instead we introduced a slightly new interface. The benefits to the alternative approach was that it's more explicit in what it will do and, most importantly, automatically works with everything without custom patches:

# implementation simplified
module Surrealist
  def self.surrealize_collection(collection)
    raise unless collection.respond_to?(:each)

    collection.map do |record|
      surrealize(record)
    end
  end
end

# Using any ORM now we can
Surrealist.surrealize_collection(User.all) # => ["{\"name\":\"Alessandro\"}"]

Monkey-patching is a powerful tool in the ruby ecosystem but wield it sparingly especially if you are going to jumble into code that does not belong to you. If you are interested here is the back and forth of this problem being solved on the Surrealist project.

Have fun monkeying around!


Questions? Free free to contact me anytime :)

Get Notified of Future Posts