3. Service Locator

3.1. Overview

The service locator design pattern can be considered a subset of dependency injection. Because it is simpler, it is as good of a place to start teaching DI as any.

To demonstrate both techniques, we’ll pretend we’re going to write an online forum application. To start, let’s come up with a rough design by cataloging the components we’ll need.

  • Logger. This will be used to write messages to a file.
  • Authenticator. This will be used to validate whether a user is who they say they are.
  • Database. This encapsulates access to the database that will store our forum data.
  • Session. This represents a single user’s session.
  • View. The presentation manager, used to render pages to the user.
  • Application. The controller that ties it all together.

(Of course, a real online forum application would be significantly more complex, but the above components will do for our purposes.)

The dependencies between these components are:

  • Authenticator has Database (for querying user authentication information) and Logger
  • Database has Logger (for indicating database accesses and query times)
  • Session has Database (for storing session information) and Logger
  • Application has Database, View, Session, and Authenticator, and Logger

3.2. Conventional Architecture

A conventional architecture will have each component instantiate its own dependencies. For example, the Application would do something like this:

A conventional application implementation [ruby]
1
2
3
4
5
6
7
8
9
class Application
  def initialize
    @logger = Logger.new
    @authenticator = Authenticator.new
    @database = Database.new
    @view = View.new
    @session = Session.new
  end
end

However, the above is already flawed, because the Authenticator and the Session both need access to the Database, so you really need to make sure you instantiate things in the right order and pass them as parameters to the constructor of each object that needs them, like so:

A parameterized application implementation [ruby]
1
2
3
4
5
6
7
8
9
class Application
  def initialize
    @view = View.new
    @logger = Logger.new
    @database = Database.new( @logger )
    @authenticator = Authenticator.new( @logger, @database )
    @session = Session.new( @logger, @database )
  end
end

The problem with this is that if you later decide that View needs to access the database, you need to rearrange the order of how things are instantiated in the Application constructor.

3.3. Locator Pattern

The service locator pattern makes things a little easier. Instead of instantiating everything in the constructor of the Application, you can create a factory method somewhere that returns the new Application instance. Then, inside of this factory method, you assign each new object to collection, and pass that collection to each constructor.

Service locator example [ruby]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
require 'needle'

def create_application
  locator = Needle::Registry.new

  locator.register( :view ) { View.new(locator) }
  locator.register( :logger ) { Logger.new(locator) }
  locator.register( :database ) { Database.new(locator) }
  locator.register( :authenticator ) {Authenticator.new(locator) }
  locator.register( :session ) { Session.new(locator) }
  locator.register( :app ) { Application.new(locator) }

  locator[:app]
end

class Application
  def initialize( locator )
    @view = locator[:view]
    @logger = locator[:logger]
    @database = locator[:database]
    @authenticator = locator[:authenticator]
    @session = locator[:session]
  end
end

class Session
  def initialize( locator )
    @database = locator[:database]
    @logger = locator[:logger]
  end
end

...

This has the benefit of allowing each object to construct itself la carte from the objects in the locator. Also, each object no longer cares what class implements each service—it only cares that each object implements the methods it will attempt to invoke on that object.

Also, because Needle defers the instantiation of each service until the service is actually requested, we can actually register each item with the locator in any arbitrary order. All that is happening is the block is associated with the symbol, so that when the service is requested, the corresponding block is invoked. What is more, by default each service is then cached, so that it is only instantiated once.

Thus, when we get the :app service (on the last line), the Application constructor is invoked, passing the locator to the constructor. Inside the constructor, Application retrieves each of its dependencies from the locator, causing each of them to be instantiated in turn. By this means, everything is initialized and constructed when the create_application method returns.

In the interest of brevity, the create_application could have been written like this, using a “builder” object (called b in the example below) to help register the services:

Service locator example using #define [ruby]
1
2
3
4
5
6
7
8
9
10
11
12
def create_application
  locator = Needle::Registry.define do |b|
    b.view { View.new(locator) }
    b.logger { Logger.new(locator) }
    b.database { Database.new(locator) }
    b.authenticator {Authenticator.new(locator) }
    b.session { Session.new(locator) }
    b.app { Application.new(locator) }
  end

  locator[:app]
end