ActiveSupport Decorators
The decorator pattern is particularly useful when extending constants in rails engines or vice versa. To implement the decorator pattern, you need to load the decorator after the original file has been loaded. When you reference a class in a Rails application, ActiveSupport will only load the first file it finds that matches the class name. This means that you need to manually load the additional (decorator) file or eager load all of them on application startup. This is a tiny gem that provides you with a simple way to tell ActiveSupport to load your decorator files when needed.
Installation
Add it to your Gemfile and run bundle install:
gem 'activesupport-decorators', '~> 1.0'
Example 1 - Engine extends application class.
Your main rails application defines a model called Pet (in app/models/pet.rb):
class Pet < ActiveRecord::Base
end
Your rails engine adds the concept of pet owners to the application. You extend the Pet model in the engine with the following model decorator (in my_engine/app/models/pet_decorator.rb). Note that you could use 'class Pet' instead of 'Pet.class_eval do'.
Pet.class_eval do
belongs_to :owner
end
Now tell ActiveSupportDecorators to load any matching decorator file in my_engine/app when a file is loaded from app/. A convenient place to do this is in a Rails initializer in the engine:
module MyEngine
module Rails
class Engine < ::Rails::Engine
initializer :set_decorator_dependencies do |app|
ActiveSupportDecorators.add("#{app.root}/app", "#{config.root}/app")
end
end
end
end
Example 2 - Application extends engine class.
Similar to the example above except the initializer is placed in the main application instead of the engine. Create a file called config/initializers/set_decorator_dependencies.rb (or any other name) with content:
ActiveSupportDecorators.add("#{MyEngine::Rails::Engine.root}/app", "#{Rails.root}/app")
Example 3 - Engine extends another engine class.
module MyEngine
module Rails
class Engine < ::Rails::Engine
initializer :set_decorator_dependencies do |app|
ActiveSupportDecorators.add("#{AnotherEngine::Rails::Engine.root}/app", "#{MyEngine::Rails::Engine.root}/app")
end
end
end
end
Debugging
Need to know which decorator files are loaded? Enable debug output:
ActiveSupportDecorators.debug = true
Comparison to other gems
Other gems work by simply telling Rails to eager load all your decorators on application startup as seen here and here. They expect your decorators to use 'MyClass.class_eval do' to extend the original class as this is what triggers the original class to be loaded. Disadvantages of this approach include:
- decorators may not expect other classes to be decorated already, it's a bad idea to depend on load order.
- development mode is a bit slower since your decorator eager loading usually has a cascade effect on the application. This is more noticeable when using JRuby as it will be a compile action instead of class load action.
- using 'MyClass.class_eval do' instead of 'class MyClass' means you can not define constants.
This gem works by hooking into ActiveSupport, which means that decorators are loaded as required instead of at application startup. You can use 'class MyClass' and expect that other classes are already decorated, since when you reference other classes they will be decorated on the fly when ActiveSupport loads them.