Last active
October 21, 2022 12:25
-
-
Save andypike/82eb791e948171e6ad4e to your computer and use it in GitHub Desktop.
Self registering classes that works within a Rails app without turning on eager_loading loading in development
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# ./config/environments/development.rb | |
config.eager_load = false | |
# ./lib/registry.rb | |
module Registry | |
def self.included(base) | |
base.send(:extend, ClassMethods) | |
end | |
module ClassMethods | |
def strategies(options = {}) | |
self.default = options.fetch(:default) | |
self.registry = {} | |
end | |
def register(strategy) | |
registry[strategy.identifier] = strategy | |
end | |
def strategy_for(identifier) | |
SelectiveEagerLoader.ensure_loaded | |
registry.fetch(identifier) { default } | |
end | |
private | |
attr_accessor :registry, :default | |
end | |
end | |
# ./lib/selective_eager_loader_base.rb | |
class SelectiveEagerLoaderBase | |
cattr_accessor :loaded | |
def self.ensure_loaded | |
return if already_loaded? | |
Rails.logger.info("Selectivly eager loading types") | |
files.each { |file| require_dependency(file) } | |
loaded! | |
end | |
def self.files | |
Dir.glob( | |
paths.map{ |path| Rails.root.join(path, "**/*.rb") } | |
) | |
end | |
def self.already_loaded? | |
loaded? || Rails.configuration.eager_load | |
end | |
def self.loaded? | |
loaded | |
end | |
def self.loaded! | |
self.loaded = true | |
end | |
end | |
# ./lib/self_registering.rb | |
module SelfRegistering | |
def self.included(base) | |
base.send(:extend, ClassMethods) | |
end | |
module ClassMethods | |
attr_accessor :identifier | |
def works_with(identifier, options = {}) | |
self.identifier = identifier | |
options.fetch(:registry).register(self) | |
end | |
end | |
end | |
# --------------------------------------------------------------------- | |
# ./app/models/selective_eager_loader.rb | |
class SelectiveEagerLoader < SelectiveEagerLoaderBase | |
def self.paths | |
["app/models/tools"] | |
end | |
end | |
# ./app/models/toolbox.rb | |
class Toolbox | |
include Registry | |
strategies :default => Tools::BareHands | |
end | |
# ./app/models/tools/anvil.rb | |
module Tools | |
class Anvil | |
include SelfRegistering | |
works_with :metal, :registry => Toolbox | |
def build_something | |
puts "I'm an anvil and I can work with metal" | |
end | |
end | |
end | |
# ./app/models/tools/saw.rb | |
module Tools | |
class Saw | |
include SelfRegistering | |
works_with :wood, :registry => Toolbox | |
def build_something | |
puts "I'm a saw and I can work with wood" | |
end | |
end | |
end | |
# ./app/models/tools/bare_hands.rb | |
module Tools | |
class BareHands | |
def build_something | |
puts "I use my bare hands and work with anything else" | |
end | |
end | |
end | |
# ./app/controllers/tools_controller.rb | |
class ToolsController < ApplicationController | |
def index | |
tool = Toolbox.strategy_for(:wood) | |
puts tool.new.build_something | |
tool = Toolbox.strategy_for(:metal) | |
puts tool.new.build_something | |
tool = Toolbox.strategy_for(:fabric) | |
puts tool.new.build_something | |
end | |
end | |
# => I'm a saw and I can work with wood | |
# => I'm an anvil and I can work with metal | |
# => I use my bare hands and work with anything else |
Hi, I was trying to build something to register a set of classes in Rails when I came across this gist. I would like to use your code, but there doesn't seem to be a license attached. What is your policy on this?
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Background
This is an extension to https://gist.github.com/andypike/6fd735862302b09f5259 (check that out first).
That solution works fine in a simple Ruby app but if you try to use it in a Rails app then it doesn't work in development or test environments. If you try it, the
Toolbox
registry will always return the default ofBareHands
. Why is this? Well, it's due to how Rails loads classes in development. If you look in./config/environments/development.rb
you will see the default config ofconfig.eager_load = false
. This basically tells Rails to lazy load classes as they are needed. Now, as our tools are not directly used by any of our code then they are never loaded and so never self register withToolbox
. You can fix that by changing the config toconfig.eager_load = true
which will load all classes in the load path at boot time. This of course then slows down development/test boot time. Also, if you modify any source file in development while the server is running, then Rails will reset and lazily load the classes. This has the affect ofToolbox
forgetting your tools and then again always returning the default ofBareHands
after you make a change.To fix this we need to let Rails know about the tools and get them loaded lazily if they are needed (without changing the
config.eager_load
setting in development or test environments).How does the above work?
In a change from the previous example, we have now created a
SelectiveEagerLoader
class that allows you to specify a list of folders that contain files that should be loaded. Then in the controller, when we ask for a tool for the first time thenToolbox
will tellSelectiveEagerLoader
load all of the*.rb
files in the supplied paths via Railsrequire_dependency
which requires the file and marks it as auto-loaded.Therefore, in development you can change code and the tools will self-register when the first one is resolved and everything else works as expected when
config.eager_load = false
.This example also generalises the modules which allows you to have multiple registries and there is no type hard coding.
As with the previous example, if you need to support a new strategy, all you need to do is add a new tool class to the tools folder. Doing this in development just works without restarting the server or needing
eager_load
set totrue
.