Skip to content

Instantly share code, notes, and snippets.

@andypike
Last active October 21, 2022 12:25
Show Gist options
  • Save andypike/82eb791e948171e6ad4e to your computer and use it in GitHub Desktop.
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
# ./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
@andypike
Copy link
Author

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 of BareHands. 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 of config.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 with Toolbox. You can fix that by changing the config to config.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 of Toolbox forgetting your tools and then again always returning the default of BareHands 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 then Toolbox will tell SelectiveEagerLoader load all of the *.rb files in the supplied paths via Rails require_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 to true.

@sonya
Copy link

sonya commented Aug 11, 2017

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