This content originally appeared on DEV Community and was authored by Jeremy Friesen
Steps for Patching in a Responsible Manner
I previously published this in at a different site, but I think it remains helpful for those working in Ruby.
In Ruby, objects and classes are open for extension. Even after you’ve declared a class, that Ruby class and (it’s instantiated objects) continue to be open for extension (e.g. adding new methods, including new modules, etc.).
Rails leverages this to great effect (e.g. 2.days.ago
). Knowing this can help you shore up a problem.
The following example provides a tool in your Ruby toolkit to perhaps help you avoid creating a fork of an upstream dependency. In an ideal world, you’d be able to submit a patch to the upstream project, wait for the next release, and then update your local machine.
However, the ideal is not always available. So I want to walk through an approach that I’ve used a handful of times.
Example of Extending
Nestled deep in a dependent gem, you have a method you need to change. For some reason the implementation details are inadequate for your needs.
The Setup
In the dependent gem, using pure Ruby you might see something like this:
module Deep
module Gem
module Base
def the_method_to_change
# Business logic that isn't quite in line with your business logic.
end
end
end
end
module Deep
class Object
# Adds the **instance methods** of the extended module as class methods to
# this object. `Deep::Object.the_method_to_change` works but
# `Deep::Object.new.the_method_to_change` would raise a MethodMissing Error
extend Deep::Gem::Base
end
end
In Rails world, leveraging ActiveSupport::Concern, you might see the following:
module Deep
module Gem
module Base
extend ActiveSupport::Concern
class_methods do
def the_method_to_change
# Business logic that isn't quite in line with your business logic.
end
end
end
end
end
module Deep
class Object
# Note, we are using `include` instead of `extend`, but the effect is the
# same; `Deep::Object.the_method_to_change` works but
# `Deep::Object.new.the_method_to_change` would raise a MethodMissing Error
include Deep::Gem::Base
end
end
In your application you may be stuck calling Deep::Object.the_method_to_change
yet want to update the underlying implementation. In the real-world example, we wanted to override the behavior of ActiveFedora’s .reindex_everything
method.
Implementation
I recommend that if you want to change the method implementation, you make another module and extend that base class. And follow the implementation pattern of the method you are overriding - use ActiveSupport::Concern
if the upstream module uses it.
require 'deep/object'
unless Deep::Object::VERSION == '1.0.1'
raise "Verify this override is still needed for non 1.0.1 versions"
end
module MyNamespace
module Overrides
extend ActiveSupport::Concern
class_methods do
def the_method_to_change
end
end
end
end
Deep::Object.include(MyNamespace::Overrides)
The above override has a few concepts:
- Explicit Require
- ensure the upstream class definition is loaded.
- Provide Guidance
- some guidance that the assumed override only works for version 1.0.1.
- Separate Module
- to provide structure and further opportunity to document.
Explicit Require
This might be self-evident, but I want to reiterate - if you are replacing an upstream method, make sure that the method to replace is declared before you begin replacing it.
Provide Guidance
In the days of yore, I found and patched a Rails bug. I was working in Rails 3.0.x and used the above approach.
I wrote my module with a Rails version check. Each time I bumped the Rails version and rebooted Rails, the file would raise an exception saying “Go check if this fix has been applied.”
For a year or so, I walked that patch along until one day in Rails 3.2.0, the patch was in the main
branch. I deleted the file and went about my other work.
When you add that “monkey patch” file, provide context to why you are making the change; What assumptions are in play? Raise an exception if those assumptions are not valid.
- Provide guidance on how to check this assumption
- Add comments on why you are doing this
- Add information in your commit messages describing why
- And for all that is holy and sacred, write some tests that confirm your expected behavior.
Separate Module
Create a separate module; This allows documentation on the nature of the module. Maybe the module contains interrelated overrides for multiple classes; Or you have a single override. Regardless, this gives a place for people to expect changes.
By mixing in another module, you preserve access to the super
method. In the above example, I could add to the the_method_to_change
definition a call to super
and it call the original Deep::Object.the_method_to_change
method.
An Inadequate Implementation
You can see an “in the wild implementation” in an application I once helped maintain. I added the config/initializers/active_fedora_soft_delete_monkey_patch.rb
file to contain the logic for soft-deletes. The implementation details spanned two inter-related gems, but the logic in our application was inter-related. And for those keeping score, I didn’t quite follow all of my own advice.
I’m not saying that the best place for these changes is in an initializer, but I do believe you should put them in a discoverable place (where-ever that might be in a large code-base).
Conclusion
Knowing the capabilities of your language can help you address a problem in the immediate moment and equip you to best “own” that short-cut.
The collective Ruby community has spilled a lot of digital ink posting about Extend vs Include and the nuances. Some posts to consider:
And a personal favorite by Jay Fields for alternatives ways to redefine Ruby methods. Seriously this blog post was what hooked me on Ruby; Methods are detachable and re-attachable lambdas.
Postscript
Since the original publication of this post (and the even older days of using this approach), Ruby has developed other methods of leveraging a module
. The prepend
method in particular. See Rails 5, Module#prepend, and the end of alias_method_chain
.
This content originally appeared on DEV Community and was authored by Jeremy Friesen
Jeremy Friesen | Sciencx (2022-07-06T21:36:15+00:00) Responsible Monkey Patching of Ruby Methods. Retrieved from https://www.scien.cx/2022/07/06/responsible-monkey-patching-of-ruby-methods/
Please log in to upload a file.
There are no updates yet.
Click the Upload button above to add an update.