The Registry Pattern
Using a Registry to look up collaborators in Rails, like external APIs.
Avdi Grimm published a great RubyTapas episode last year on the Registry pattern. To summarize: the Registry pattern (sometimes called a Service Locator) is a way for classes to look up their collaborators, rather than require they instantiate their collaborators explicitly. This simple bit of indirection can go a long way to making code easier to write and test.
Avdi gave a simple Ruby example, but it made me think: how might I do something similar in a Rails app?
An example
Let’s say we’re building a Rails app that needs to collaborate with a REST API that is provided by the IT department at our organization. Our IT department is a bit paranoid, and wants to be notified when users perform password resets. No problem. We’ve already wrapped their API in a simple Ruby class that works like this:
auditor = SecurityAuditor.new(:api_key => ENV["SECURITY_LOGGER_API_KEY"])
auditor.log_password_reset(username, ip_address)
Imagine that this SecurityAuditor
also exposes other methods that need to be hooked in to various parts of our controllers or models. How do we make this object available throughout our app?
Global variable
A “brute force” approach would be to create the auditor in config/initializers
and make it available as a global variable. Indeed, I’ve seen this done in Rails occasionally with things like $redis
or $memcache
for global access to those external services. This definitely works, but globals are usually frowned upon. Let’s explore some other options.
Inline
Instead, I could instantiate the SecurityAuditor within each model or controller whenever I need it. This works too, but now I’m repeating code everywhere. If the SecurityAuditor performs its own caching or allocates resources like persistent HTTP connections, this could be really inefficient as well. Not good.
Application controller
A third option that I’ve seen in Rails apps is to create and memoize the SecurityAuditor in the ApplicationController
. That means the auditor is getting created at most once per request, which might be acceptable. Controllers now have easy access to the auditor, but for models, I would have to pass the auditor as an argument to every model call that needs it. Messy. And if my models are being invoked indirectly (e.g. via Devise), that might be difficult.
The registry approach
Here’s the approach that I’ve come to use in my recent projects, inspired by Avdi’s Registry episode.
First, I create a Registry
module (or MyAppNameRegistry
) that provides module functions for accessing service objects, like the SecurityAuditor
in my example. The delegate
declaration is a bit of syntactic sugar that I’ll explain later.
module Registry
delegate :security_auditor, :to => self
def self.security_auditor
@security_auditor ||= SecurityAuditor.new(
:api_key => ENV.fetch("SECURITY_AUDITOR_API_KEY")
)
end
end
Then, I use it like this:
Registry.security_auditor.log_password_reset(username, ip_address)
The delegate
macro allows me to use the Registry as a mixin, which makes accessing the auditor more concise. This can be used to avoid repetition when the auditor is used multiple times within a single class.
class User < ActiveRecord::Base
include Registry
def reset_password
# Delegates to Registry.security_auditor
security_auditor.log_password_reset(...)
end
end
The Registry
itself is essentially a global singleton, so it shares the benefit of simplicity with the global variable approach. But it gives us more safety (reassigning the Registry
constant causes a warning), and more flexibility: the security_auditor
method can do whatever it wants to obtain an instance of the SecurityAuditor
. Here are some possibilities:
- Memoize and share one instance across the entire application for efficiency (as in the example above).
- Use the thread local pattern to ensure each thread gets its own SecurityAuditor instance, in case the auditor is not thread-safe.
- Return a mock during testing.
Where the registry pattern falls down
The Registry pattern is not always needed, and in some cases it can be the wrong solution. If you are writing code that will eventually be distributed outside of your app, you can’t assume that the global Registry will always be present. In this case, adding attribute writers or initializer arguments to pass in collaborators (i.e. dependency injection) is a more verbose but foolproof approach.
You just read
The Registry Pattern
Using a Registry to look up collaborators in Rails, like external APIs.
Share this post? Copy link
About the author
Hi! I’m a Ruby and CSS enthusiast, regular open source contributor, software engineer, and occasional blogger writing from the San Francisco Bay Area. Thanks for stopping by! —Matt