Introducing SanePatch
How to patch your gems like a pro
11 June 2019
~ 11 minutes
Looking for the gem?
If you’re not interested in my thoughts about monkey patching you might wanna jump straight to the gem on GitHub.
If you’re a ruby programmer chances are high that you’ve encountered the practice of monkey patching classes and modules.
While monkey patching is often seen as an unprofessional and not desirable praxis of changing behaviour of existing code, it also provides a sharp knife that allows programmers to make a trade between the freedom and the handholding provided by the ruby programming language.
Applied correctly and with care, monkey patching becomes a powerful tool that every ruby programmer can benefit from.
When should I use monkey patching?
There are a few things that you should ask yourself before you try to solve a problem with monkey patching.
Do you really need this patch to be global?
One common use of monkey patching is to add helper methods to core classes such as String
, Integer
or Hash
. In some cases those monkey patches are incredible useful, such as the Numeric#days
method added by rails. But often developers add rather domain specific methods to those classes.
Let’s take a look at an example from a real world rails application written by an inexperienced rails programmer, that just discovered the magic of monkey patching - also known as me from the past.
The following code was used to turn a string like ProductExtension::ApplicableMaterial
into Material
.
class String
def extensionize
self.demodulize # Removes the module part 'ApplicableMaterial'
.underscore # replaces CamelCase with underscores 'applicable_material'
.humanize # Replaces alls underscores with spaces 'applicable material'
.titleize # Titalizes all Words 'Applicable Material'
.split # Turns the string in an array ['Applicable', 'Material']
.last # Returns the last element of the Array 'Material'
end
end
This code could have been way simpler and memory friendlier by utilising regex, but the real problem of this method is that it lives on the core String
class.
See, the extensionize
method is only used about 3 times in the whole code base and all of those calls happen from the exact same class!
The String#extensionize
is really only used to dynamically build a belongs_to
relationship inside this concern:
module ProductExtension::Applicable
extend ActiveSupport::Concern
included do
belongs_to :configuration,
class_name: "ProductExtension::#{name.extensionize}Configuration"
end
end
Instead of monkey patching this method on to the String
class, it could simply be a method on the ProductExtension
module. That way its declaration would be closer to its actual usage and no monkey patching would be necessary.
Are you breaking any contracts?
Another common form of monkey patching is changing existing methods on existing classes instead of adding new ones to them.
When monkey patching existing methods, you should always be aware of how stable or unstable the method is that you want to patch.
If you’re patching unstable methods that are part of a private API, chances are high that your monkey patches will break without any warnings, even after the smallest updates to your gems. You probably should not monkey patch code that is not part of the public API, but if you need to, I added some tips in the next section that you might find useful.
If you’re patching stable methods such as a method belonging to a public API of a gem that uses semantic versioning or similar ways that guarantee that the methods behaviour won’t change without you noticing it you run into a whole different problem; Your patch breaking other gems.
Core classes such as String
or Object
are extremely stable, meaning that updates almost never include breaking changes for those classes. This has the side effect that A LOT of code in gems, frameworks or even the ruby standard library rely on the exact behaviour of those methods.
The simplest and smallest changes to one of those stable methods can change the behaviour of the method in a way that breaks your whole application or (even worse) introduce very strange and hard to trace bugs.
How do you make sure that your patch keeps working?
Maybe you’re not trying to patch a very stable core class such as String
but you’re actually trying to patch a method that belongs to a somewhat documented public API or even a private API.
In this case you not only have to be careful to not break the contract of the method for callers of the method, but you also need to ensure that the methods behaviour doesn’t change in a way that renders your patch useless or breaks it.
The correct way to validate your monkey patches is to write test for them that ensure that they behave exactly the way they should, but I gotta be honest to you.. Sometimes your patches aren’t easy to test.
Your patches might improve the performance of the method, fix a security issue or a bug that is very hard to reproduce in a test.
You might even know that your patch will not be needed anymore, once the next version of the gem you’ve patched is released since the bug is fixed or the performance has been improved.
If you don’t have proper tests for the patch for the reasons I mentioned above or because you just were to lazy it’s very easy to end-up with a patch that is either not necessary anymore or even harms you.
Using a version guard
An easy and effective solution to this problem is to wrap your patch inside a guard that checks if the gems version has been changed. This will remind you to manually double check that your patches are still needed and that they are working once the gem you’ve patched is upgraded (or downgraded).
I first encountered the idea of such a version guard when I was assigned the task of performing a minor rails upgrade of our product and was greeted with exceptions that warned me about existing patches that need to be verified. This was the moment I realised that monkey patching can actually be done in a safe and sane way.
I now use this approach of guarding patches with a specific gem version quite often. Often enough that I recently built a small helper that allows to easily guard monkey patches.
SanePatch.patch('<gem name>', '<current gem version>') do
# Apply your patches here the same way as usual.
end
This helper can be used to wrap any code and will raise if your gem version has changed:
Greeter.greet # => 'Hello'
# Let's patch the `Greeter.greet` method to output 'Hello Folks'
module GreeterPatch
def greet
"#{super} Folks"
end
end
# We currently have version 1.1.0 of the greeter gem
SanePatch.patch('greeter', '1.1.0') do
Greeter.prepend(GreeterPatch)
end
Greeter.greet # => 'Hello Folks'
If somebody updates the gem version the patch will raise as soon as its code path is executed:
SanePatch::Errors::IncompatibleVersion:
It looks like the greeter gem was upgraded.
There are patches in place that need to be verified.
Make sure that the patch at initializers/greeter_patch.rb:8 is still needed and working.
Complex version constraints
Let’s be honest. Sometimes life isn’t easy. But it’s not the only thing that can be complex, sometimes you know that your patch won’t be needed anymore in a specific version of the gem, or you might know that your patch will be needed and will stay working for all minor releases of a specific gem version.
Both, life and your needs for sane patching can get complex. Luckily, SanePatch supports all version constraints you know and love from RubyGems. This means that you can do things like the following:
SanePatch.patch('greeter', '~> 1.1.0') { # your patch }
SanePatch.patch('greeter', '> 1.1.0') { # your patch }
SanePatch.patch('greeter', '< 1.1.0') { # your patch }
If you’re fixing a bug you might know the version in which the bug was introduced and the version in which it was fixed. In such situations it can make sense to add a version constraint that expresses this:
SanePatch.patch('greeter', '> 1.0.0', '< 1.1.0') { # your patch }
Providing additional information
If you patch a known bug in a gem it might be useful to provide additional information why the patch is needed and when it can be removed.
Greeter.silence # => nil
module GreeterPatch
def silence
''
end
end
details = <<-MSG
The `silence` method should output an empty string rather than nil.
This is a known issue and will be fixed in the next release.
See: https://github.com/Jcambass/greeter/issues/45
MSG
SanePatch.patch('greeter', '1.1.0', details: details) do
Greeter.prepend(GreeterPatch)
end
Greeter.silence # => ''
The additionally provided details will also show up in the exception message.
SanePatch::Errors::IncompatibleVersion:
It looks like the greeter gem was upgraded.
There are patches in place that need to be verified.
Make sure that the patch at initializers/greeter_patch.rb:8 is still needed and working.
Details:
The `silence` method should output an empty string rather than nil.
This is a known issue and will be fixed in the next release.
See: https://github.com/Jcambass/greeter/issues/45
Start using SanePatch in your project
If you like the idea of a small helper that allows you to guard your patches you should give SanePatch
a try.
Browse the code on GitHub, download it on RubyGems or include it in your projects Gemfile:
gem 'sane_patch', '~> 0.2'
Is there another way that could solve your problem?
As you probably have figured out by now I’ve mainly written this blog post to announce SanePatch
to the ruby community.
But it’s important to stress that monkey patching shouldn’t be your first choice and you should always at least research what other options are available to fix your problem before you reach for the sharp monkey patch knife. Sometimes all you really need is a very safe, boring and child friendly butter knife.