Ruby is the language of monkey patching. While it’s theoretically possible to avoid monkey patching, I’m 99% sure that your app contains at least a few of them (especially of you use Rails).
Monkey patching is usually considered as an anti-pattern, but sometimes it can’t be avoided.
The typical use case of monkey patching in a Rails app is a bug fix. If you’re using Rails 5 and the bug you’re hunting was only fixed in 5.1 which is not released yet, you’d have no other option than to copy-paste the fix and use a monkey patch.
But still, there are good and bad ways to add a monkey patch. This Pull Request introduces a configurable option for dumping a database. Before that patch, there was no way to configure dumping flags. Imagine that we’re using an older Rails and we still want to pass a custom flag. We’d have to monkeypatch that class:
# config/initializers/active_record_patches.rb module ActiveRecordDbCommandPatch def run_cmd(cmd, args, action) # pass an extra flag to mysqldump if cmd == "mysqldump" args = args + ["—skip-add-drop-table"] end super(cmd, args, action) end end ActiveRecord::Tasks::MySQLDatabaseTasks.prepend(ActiveRecordDbCommandPatch)
Why is this way to monkey patch is not the best? Because when we upgrade to a new Rails version that has a configurable option, we may forget to clean up and this patch will still live in the app. Even worse, imagine that
run_cmd method in Rails was refactored and the patch will introduce a bug.
We can improve it by 1) checking that
run_cmd is available and 2) that configurable option is not available yet in the current Rails version.
# config/initializers/active_record_patches.rb if ActiveRecord::Tasks::DatabaseTasks.respond_to?(:structure_dump_flags) raise "you're running the Rails version that no longer requires the patch" end module ActiveRecordDbCommandPatch def run_cmd(cmd, args, action) # pass an extra flag to mysqldump if cmd == "mysqldump" args = args + ["—skip-add-drop-table"] end super(cmd, args, action) end end # instance_method will raise with NameError is the method is not available if ActiveRecord::Tasks::MySQLDatabaseTasks.instance_method(:run_cmd) ActiveRecord::Tasks::MySQLDatabaseTasks.prepend(ActiveRecordDbCommandPatch) end
This way will help you to remove the patch as soon as you update Rails. There is also a way to use the Rails version as an indicator that the patch is no longer necessary:
if Rails::VERSION::MAJOR > 4 raise "you're running the Rails version that no longer requires the patch" end
For a large Rails app, it may be impossible to avoid monkeypatches. The best we can do is to inject them carefully, providing a safe way for a patch to be removed when it’s no longer necessary.