Monkey patching in Ruby isn’t always what it seems. Sometimes you’re doing it the right way for the wrong reasons, other
times you are doing it just flat out wrong. Hopefully, after reading this post you’ll have a better idea what you’re
doing, and you’ll be monkey patching the right way for the right reasons.
Let’s create a hypothetical scenario in which I need to monkey patch the Hash class. In this situation, I need to
accomplish two things:
- Allow JSON strings to be interpolated by Hash’s
try_convert
class
method
- Pretty print JSON using a new method on a Hash instance, namely
to_pretty_json
When I’m done I should be able to run the following and get the associated output (where hash_ext.rb
is our Hash
extension file):
require './hash_ext.rb'
Hash.try_convert('{"a": 1}')
# => {"a"=>1}
puts({"a" => 1}.to_pretty_json)
# {
# "a": 1
# }
There are three main ways to do this:
- Re-opening the Hash class to define/redefine those methods
- Including a module in the Hash class that contains the new/updated methods
- Prepending a module to the Hash class that contains the new/updated methods
Re-opening the Hash class
This is the classic example of how versatile Ruby can be, re-opening a class or module and adding or updating methods.
It’s used a lot in showing the power of Ruby to newcomers, but can have some nasty side effects, so this method is
rarely used by libraries or production systems.
As the original try_convert
method is a Ruby wrapper around a C binding, we need to be careful when redefining it.
Hash’s try_convert
docs simply say:
Try to convert obj
into a hash, using to_hash
method. Returns converted hash or nil
if obj
cannot be converted
for any reason.
It’s a pretty simple method, so if we wanted to, we could write our own Hash#try_convert
from scratch. Something like
this would do the trick:
class Hash
def self.try_convert(obj)
obj.to_hash rescue nil
end
end
Then, it would be extremely easy to adjust the logic to support our new use case:
require 'json'
class Hash
def self.try_convert(obj)
result = obj.to_hash rescue nil
result || JSON.parse(obj) rescue nil
end
end
For a simple method like this, this approach will work, but for cases where the original method is complicated or is
going to/can change, this form of monkey patching will come back to haunt you. Say, for example, the original
try_convert
’s logic was changed, or an exploit was patched. You would have to be consistently monitoring changes to
try_convert
and replicating the change in your patch code anytime a change was made to the upstream. Not very
scalable. There is, however, another option. Instead of overriding the method directly, we can alias the original
method, and still access it under a different name:
hash_ext.rb
require 'json'
class Hash
class << self
alias_method :original_try_convert, :try_convert
def try_convert(obj_or_json_string)
original_try_convert(obj_or_json_string) ||
JSON.parse(obj_or_json_string) rescue nil
end
end
def to_pretty_json
JSON.pretty_generate(self)
end
end
If we were to monkey patch Hash by reopening the Hash class, this would be the most appropriate form of it. Ideally, we
would never monkey patch like this, as it comes with some pretty serious side effects.
- Easy to write, easy to understand.
- Chance of alias overwriting existing method or new method overwriting your alias.
super
doesn’t point to the original method. We have to either alias the original method or reproduce its logic.
- Replicating logic means that if the original logic is updated, we’ll have to update our monkey patch.
- If the class or method hasn’t been defined before re-opening it, you will inadvertently create it.
Including a module into the base class or module is by far the most popular way libraries and production systems monkey
patch in Ruby. It comes with some really nice tools, like the included
hook method which gets run each time your
module is included in a class or module.
With this included
hook, instead of including instance methods and singleton methods separately, we can use the hook
when including the instance methods, to also attach the class methods.
hash_ext.rb
require 'json'
module HashExt
def self.included(base)
base.instance_eval do
singleton_class.send(:alias_method, :original_try_convert, :try_convert)
def try_convert(obj_or_json_string)
original_try_convert(obj_or_json_string) ||
JSON.parse(obj_or_json_string) rescue nil
end
end
end
def to_pretty_json
JSON.pretty_generate(self)
end
end
Hash.include(HashExt)
Using this form of monkey patching comes with a couple of the same issues from re-opening the class, and a couple of new
smells. The fact that we are using a send
just so we can utilise the original method is a pretty big code smell.
Again, we could replicate the logic of the original method, but that comes with some pretty significant downsides as we
just found out.
This form monkey patching comes with a couple of benefits too. If we have a similar class, which we want to apply the
same patches to, we can simply just add MyClass.include(HashExt)
to the bottom of hash_ext.rb
.
- An exception will be raised if base class or module isn’t defined.
- Chance of alias overwriting existing method or new method overwriting your alias.
super
doesn’t point to the original method. We have to either alias the original method or reproduce its logic.
- Replicating logic means that if the original logic is updated, we’ll have to update our monkey patch.
- Requires sending the
alias_method
method to the singleton class.
Prepending modules in Ruby is a more badass form of including them. It comes with a hook like include’s, only it’s
called prepended
instead of included
. The big difference is that prepend and include insert your module differently
into the ancestry chain. I go into more detail on what that means in my post
on Include vs Prepend, but for now, just be aware that it opens up the
opportunity to use super
in our custom module.
Using super
in our module means we don’t have to alias the original method, instead, we can override it and use
super
to reference the original method. This makes our extension module a lot clearer and a lot easier to read and
manage.
hash_ext.rb
require 'json'
module HashExt
def self.prepended(base)
base.singleton_class.prepend(ClassMethods)
end
module ClassMethods
def try_convert(obj_or_json_string)
super || JSON.parse(obj_or_json_string) rescue nil
end
end
def to_pretty_json
JSON.pretty_generate(self)
end
end
Hash.prepend(HashExt)
- Don’t have to pollute the base class or module with aliased methods when overwriting base class methods.
- An exception will be raised if base class or module isn’t defined.
super
points to the original method in the class or module.
- Not as simple as re-opening the class.
Although include
wasn’t ideal for our scenario, it still has many uses. It is very useful if you want to define common
logic, and want the base class you’re including into to overwrite that logic (almost like a superclass with a very small
segment of logic). In fact, Rail’s
ActiveSupport::Concern
encourages behaviour like this
as uses include
behind the scenes.
For the scenario we proposed above, where we were overwriting and extending logic defined in Hash, prepend
was the
right choice. It allowed us to add our new logic without making a mess by offering the use of super
instead of adding
aliased methods into our base class.
For more information on include
vs prepend
, please take a look at my post comparing the
two: Include vs Prepend.
For more information on the different Module hook methods like included
and prepended
, take a look at this
comprehensive article on Ruby’s hook methods.
If you think I’ve made a mistake/am completely and utterly wrong/just want to provide some feedback, please reach out.
TL;DR: When overwriting someone else’s library/code, prepend
your extension module to their class, don’t include
it.