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?
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
$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:
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
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.