As of 2022-09-13 Tue, there's a Responsible Overrides repository that has an updated demonstration of the problem this is trying to resolve.
Sometimes we need to add or modify code that doesn't exist in our app. In my experience this is most often seen in Samvera so I'll be using it in this example, but is applicable to ruby entirely.
In this example, the original behavior of the FeaturedWorkList
is inadequate for the needs of our application. The FeaturedWorkList
is defined in another gem but we want to extend it in our application. Perhaps we want the new method #work_presenters
or we want to fully override that method.
There are two steps to perform the override:
The following code will add the instance method #work_presenters
to FeaturedWorkList
.
module FeaturedWorkListDecorator
def work_presenters
ability = nil
Hyrax::PresenterFactory.build_for(ids: ids,
presenter_class: Hyku::WorkShowPresenter,
presenter_args: ability)
end
end
::FeaturedWorkList.prepend(FeaturedWorkListDecorator)
To override a class method, you would instead use ::FeaturedWorkList.singleton_class.send(:prepend, FeaturedWorkListDecorator)
.
In the SoftServ team meeting we re-affirmed that we would put the decorator file in the same directory path as the object we're overriding/decorating. In the above scenario the definition of FeaturedWorkList
is in app/views/featured_work_list.rb
. In our application we would put FeaturedWorkListDecorator
in app/views/featured_work_list_decorator.rb
.
In some situations, you might see .class_eval
used. To do the above, it would look as follows:
FeaturedWorkList.class_eval do
def work_presenters
ability = nil
Hyrax::PresenterFactory.build_for(ids: ids,
presenter_class: Hyku::WorkShowPresenter,
presenter_args: ability)
end
end
If it's a module override, use .module_eval
instead. Note that the file can still be named app/views/featured_work_list_decorator.rb
, even though .class_eval
and .module_eval
act on the original class or module name.
In either case, please make sure to include documentation explaining why you added the override. That documentation should include:
The above steps are to help future contributors understand the implications of the override and whether or not that override can be removed. We might be able to remove the override if the upstream dependency has incorprated the change.
As of we are considering the implications of recommending that you include a hard version check on your override. For example:
if Hyrax::VERSION != "2.9.6"
# To check if this override is no longer necessary, see Hyrax issue #12345
raise "The following override was implemented against Hyrax 2.9.6"
end
module FeaturedWorkListDecorator
...
end
In the .class_eval
approach, you don't need a new module. One primary difference between the .class_eval
and .prepend
is that .prepend
is how each of them treat super
.
For .prepend
when you call super
you would call the original FeaturedWorkListDecorator#work_presenters
. For .class_eval
there may not be a super
method.
For the above decorator to be evaluated (and thus the code "live"), you will need to ensure that the application knows about it and loads it.
For a Rails application, ensure that the following is in config/application.rb
:
module MyApp
class Application < Rails::Application
config.to_prepare do
...
# Allows us to use decorator files
Dir.glob(File.join(File.dirname(__FILE__), "../app/**/*_decorator*.rb")).sort.each do |c|
Rails.configuration.cache_classes ? require(c) : load(c)
end
end
end
Below are some sample pull requests that implement this pattern:
find . -type f -name "*_decorator.rb"
.config.to_prepare
block? :: The Rails initialization events run in a specific order. By using the .to_prepare
moment, we ensure that we've loaded the underlying gem (e.g. FeaturedWorkList
) and can then override it.Rails.autoloaders
is a newer method that may not be available in relevant Rails versions)