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?

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!

Get Notified of Future Posts

Questions? Free free to contact me anytime :)