Monkey Patching. When you first try Ruby, it’s amazing. You can add methods right to core classes! You don’t have to call Time.now.advance(days: -1), you can write 1.day.ago! It makes Ruby a joy to read and write. Until…

You hit weird bugs because a patch changed Hash.

You get confused about which code actually ran, so you can’t debug it when it breaks.

And you finally figure out that all your problems were caused six months ago, when you monkey-patched Enumerable to make one line of code five characters shorter.

But what’s the alternative? No convenience at all? Code that looks like GroupableArray.new([1, 2, 3, 4]).in_groups_of(2)? Waiting for blank? to make it into core Ruby before you’re allowed to have nice user input handling?

You don’t want to give up on monkey patches entirely. But how can you write monkey patches that won’t make you want to fire yourself for incompetence the next time you see them?

Put them in a module

When you monkey patch a class, don’t just reopen the class and shove your patch into it:

1
2
3
4
5
class DateTime
  def weekday?
    !sunday? && !saturday?
  end
end

Why not?

  • If two libraries monkey-patch the same method, you won’t be able to tell.

    The first monkey-patch will get overwritten and disappear forever.

  • If there’s an error, it’ll look like the error happened inside DateTime.

    While technically true, it’s not that helpful.

  • It’s harder to turn off your monkey patches.

    You have to either comment out your entire patch, or skip requiring your monkey patch file if you want to run code without it.

  • If you, say, forgot to require 'date' before running this monkey patch, you’ll accidentally redefine DateTime instead of patching it.

Instead, put your monkey patches in a module:

1
2
3
4
5
6
7
8
9
module CoreExtensions
  module DateTime
    module BusinessDays
      def weekday?
        !sunday? && !saturday?
      end
    end
  end
end

This way, you can organize related monkey patches together. When there’s an error, it’s clear exactly where the problem code came from. And you can include them one group at a time:

1
2
# Actually monkey-patch DateTime
DateTime.include CoreExtensions::DateTime::BusinessDays

If you don’t want the patch anymore, just comment out that line.

Keep them together

When you monkey patch core classes, you add to the core Ruby APIs. Every app with core patches feels a little bit different. So you have to have a way to quickly learn those changes when you jump into a new codebase. You have to know where your monkey patches live.

I mostly follow Rails’ monkey-patching convention. Patches go into lib/core_extensions/class_name/group.rb. So this patch:

1
2
3
4
5
6
7
8
9
module CoreExtensions
  module DateTime
    module BusinessDays
      def weekday?
        !sunday? && !saturday?
      end
    end
  end
end

would go into lib/core_extensions/date_time/business_days.rb.

Any new developer could browse through the Ruby files in lib/core_extensions and learn what you added to Ruby. And they’ll actually use those convenient new methods you wrote, instead of those methods just getting in the way.

Think through the edge cases

I don’t know why Enumerable doesn’t have a sum method. So often, I wish I could write [1, 2, 3].sum, or ["a", "b", "c"].sum, or [Article.new, Article.new, Article.new].sum… Oh.

When you monkey patch a class, you’re usually thinking about one thing you want to make easier. You want to calculate a sum of numbers, but forget that Arrays can hold other things.

Right now, it makes sense. You’d never try to calculate the average of a bunch of hashes. But when you attach methods to an object that just plain fail when you call them sometimes, you’ll confuse yourself later on.

You can deal with this in a few ways. From best to worst:

  • Handle unexpected input reasonably.

    This works well if your patch works with strings. You can get something reasonable from almost anything if you call to_s on it first. And Confident Ruby will teach you a ton of ways to deal with different kinds of input.

  • Handle the error in a clearer way.

    This could be as easy as throwing an ArgumentError with a good message when you see input you’re not expecting. Don’t depend on someone else understanding random NoMethodErrors.

  • Document the kind of input you expect in a comment.

    The other two options are better, if you can use them. But if you can’t check for edge cases inside your patch, at least document them. That way, once your caller figures out it’s your patch that’s causing their problem, they’ll have an idea of what you were trying to do.

My all-time favorite monkey patch

Finally, I want to leave you with my all-time favorite monkey patch: Hash#string_merge:

lib/core_extensions/hash/merging.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
module CoreExtensions
  module Hash
    module Merging
      def string_merge(other_hash, separator = " ")
        merge(other_hash) {|key, old, new| old.to_s + separator + new.to_s}
      end
    end
  end
end

{}.string_merge({:class => "btn"}) # => {:class=>"btn"}

h = {:class => "btn"} # => {:class=>"btn"}
h.string_merge({:class => "btn-primary"}) # => {:class=>"btn btn-primary"}

It makes attaching CSS classes to HTML elements so much nicer.

Sensible monkey patching

Monkey-patching core classes isn’t all bad. When you do it well, it makes your code feel more like Ruby. But just like any of Ruby’s sharp edges, you have to take extra care when you use them.

If you keep your patches together, group them into modules, and handle the unexpected, your monkey patches will be as safe as they can be.

What’s the best monkey patch you’ve ever written (or seen)? Leave a comment and tell me about it!

Did you like this post? You should read these:

Finished another Rails tutorial and still don’t know how to start?

Have you slogged through the same guide three times and still can't retain enough to write apps on your own?

In my free 7-part course, you’ll discover the fastest way to learn and remember new Rails ideas, so you can use them when you need them. And you'll learn to use what you already know to build your own Rails project.



Comments