Monkey patching is one of Ruby's most powerful features. It allows programmers to add methods to core classes which can result in some very elegant APIs. However it's quite easy to shoot yourself in the foot if you don't know what you're doing.
This post from Justin Weiss is a brilliant guide on how to monkey patch responsibly. However he doesn't describe how to actually include your monkey patches in a Rails app, so that's what I'm going to describe in this post.
Following Justin's advice, the implementation of all our monkey patches should go in the lib/core_extensions
directory. So we might have some files like this:
lib/core_extensions/array.rb
module CoreExtensions
module Array
def to_set
Set.new(self)
end
end
end
lib/core_extensions/hash.rb
module CoreExtensions
module Hash
def keys_as_set
Set.new(keys)
end
end
end
The lib/
directory in Rails is not autoloaded, so to apply these patches we need to run some code when our app boots. The best place to do this is to create a file called monkey_patches.rb
under config/initializers/
. All files in this directory are executed when Rails boots.
The contents of the file would look like:
# Require all Ruby files in the core_extensions directory
Dir[Rails.root.join('lib', 'core_extensions', '*.rb')].each { |f| require f }
# Apply the monkey patches
Array.include CoreExtensions::Array
Hash.include CoreExtensions::Hash
This method works fine when we're patching Ruby's core classes, but if we want to patch classes in Rails frameworks such as ActiveStorage
or ActionText
, it's a bit more tricky as those classes may not be loaded as yet when the initializers are executed.
As you might expect from Rails, there's an elegant way to hook into the load process of those classes via ActiveSupport
. So if we wanted to apply patches to ActiveStorage::Attachment
and ActionText::RichText
, we can include the following code in the monkey_patches.rb
:
ActiveSupport.on_load(:action_text_rich_text) do
ActionText::RichText.include CoreExtensions::ActionText::RichText
end
ActiveSupport.on_load(:active_storage_attachment) do
ActiveStorage::Attachment.include CoreExtensions::ActiveStorage::Attachment
end
The above code will apply our patches right after the relevant classes are loaded!
If you look at the source code for one of the above two classes, you'll see a line like this right at the bottom:
ActiveSupport.run_load_hooks :active_storage_attachment, ActiveStorage::Attachment
This is where the name of the hook we pass into the on_load
method when applying your patch is defined.
You can also run load hooks for your own app's classes in the same way to apply some configuration at boot time. A great example would be when using the adapter pattern to integrate with external services, but that's a topic for another blog post!
This post was originally published on my blog.
Top comments (2)
Great article❣️
Thank you for sharing! It's just what I was looking for :)