In Progress
Unit 1, Lesson 21
In Progress

Registry

Video transcript & code

As I've been writing code for the RubyTapas website, I've tried to keep my classes well-decoupled. Among other things, that has meant injecting all of their dependencies. For instance, here's the code for the EpisodeMapper that we discussed a few episodes back. This is a more recent version than you saw in that episode. In order to do its job, it now needs a reference to a VideoMapper as well as to a ContentPostGateway.

class EpisodeMapper
  attr_reader :gateway, :video_map
  attr_accessor :logger

  def initialize(gateway, options={})
    @gateway   = gateway
    @video_map = ???
    @id_map    = options.fetch(:id_map) { IdentityMap.new }
  end
  # ...
end

I could hard-code a call to VideoMapper.new here, but that approach has a lot of problems. It means the EpisodeMapper would have to know about all the arguments necessary to instantiate a VideoMapper, which really isn't any of its business. It means I wouldn't be able to test the EpisodeMapper in isolation by substituting a test double for the VideoMapper. Any changes to how a VideoMapper is instantiated would force changes to this class as well. And finally, it wouldn't be able to share a VideoMapper instance with other classes that need one.

@video_map = VideoMapper.new(...)

Instead, I make it possible to pass in a VideoMapper via a keyword argument. But this introduces a new problem: now I always have to explicitly pass in a VideoMapper when I instantiate an EpisodeMapper. This is inconvenient. What if I just want to play with the EpisodeMapper in the console? I don't want to have to remember to instantiate multiple helper objects just to get a working EpisodeMapper.

@video_map = options.fetch(:video_map)

So I need a sensible default for when this option isn't specified, and I want it to be a default that doesn't tightly bind the EpisodeMapper class to knowledge about the VideoMapper class.

This isn't the only time I need to solve a problem like this, either. Many of my classes need access to a logger object in order to output useful runtime information. But I don't want to have to pass loggers around everywhere whenever I create new objects in order to get them to all log to the same place.

class ContentPostGateway
  def initialize(session, options={})
    @session = session
    @logger  = options.fetch(:logger) { Logger.new($stderr) }
  end
  # ...
end

I need a centralized place to retrieve default implementations of certain common services by name, without knowing where they come from, or how they are created. What I need is a registry.

A registry object exposes methods named for each of the services it knows how to find, while hiding the setup for those services. In some programming languages I might create a new class for the registry.

class RubyTapasRegistry
  # ...
end

But in this app I opt to simply use the top-level RubyTapas module as my registry. I define module-level methods for each service I want to expose. One of these is a video_map method. It instantiates a VideoMapper object if one doesn't exist already. In the process it uses services exposed via other registry methods, such as wistia_gateway and id_map.

def self.video_map
  @video_mapper ||= VideoMapper.new(wistia_gateway, id_map: id_map)
end

Back in the EpisodeMapper class, I add code to make the @video_map collaborator use a VideoMapper sourced from the registry by default.

@video_map = options.fetch(:video_map) { RubyTapas.video_map }

Because the default code will only be executed if there is no :video_map keyword option, I've ensured that I can still test this class in isolation. In my tests I just have to be sure to pass in a test double in place of a real video_map and I can have a test that only loads the episode_mapper.rb file and nothing else.

require "spec_helper"
require "ruby_tapas/episode_mapper"

describe EpisodeMapper do
  let(:mapper) {
    EpisodeMapper.new(double(:gateway), video_map: double(:video_mapper))
  }
  # ...
end

In the episode on a "caching proxy" we've already seen how the level of indirection that a registry adds can help insulate an application from the effects of changes. In that episode, we rewrote the internals of the registry method that fetches a ContentPostGateway to instead return a CachedContentPostGateway.

def self.content_post_gateway
  scope[:content_post_gateway] ||= DPD::CachedContentPostGateway.new(
    DPD::ContentPostGateway.new(dpd_admin_session, logger: logger),
    cache)
end

Any code that used this method to get a reference to a content post gateway was oblivious to the change, and did not have to be modified to support caching of content post data.

There's another, less obvious benefit to this extra indirection layer. Partway through development, I realize that I need to be able to control the lifetime of shared services like the video_map, content_post_gateway, logger, and so on. I want each thread to have its own copy of these services, and I also want it to possible for each request to have its own copy as well.

In order to make this happen, I add getter and setter methods for a registry "scope", which will be a hash-like object. I make it default to a class I call FiberScope, which I'm not going to show today but which is implemented in terms of fiber-local variables of the kind we used in episode 161.

def self.scope
  @scope ||= FiberScope.new
end

def self.scope=(new_scope)
  @scope = new_scope
end

Then I replace all the module instance variables I was using to memoize registry services with uses of the scope instead.

def self.video_map
  scope[:video_mapper] ||= VideoMapper.new(wistia_gateway, id_map: id_map)
end

This is the only place I have to make a change. Since I've coded every class in my application to use the registry when it looks for a service that it needs, the services they use will now be automatically localized to the current thread or fiber, rather than being globally shared.

Like most of the other patterns that have gone into this application, the Registry pattern comes from the book Patterns of Enterprise Application Architecture, by Martin Fowler. By centralizing knowledge about how to find and/or instantiate various application services, the Registry Pattern makes it possible for classes in the system to find have sensible defaults for their dependencies, without tying them unnecessarily tightly to those dependencies. And it makes it possible to easily vary the lifetime and scope of those services.

Registry is one of those patterns that can feel relatively "heavyweight", but here Ruby once again shows its talent for implementing patterns with a minimum of fuss. Instead of having a whole separate class for the registry, I was able to re-use the top-level application module as my app's registry. It feels very natural to me to type RubyTapas.video_map in order to get a reference to a VideoMapper object.

And that's it for today. Happy hacking!

Responses