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
hasDatabase
(for querying user authentication information) andLogger
Database
hasLogger
(for indicating database accesses and query times)Session
hasDatabase
(for storing session information) andLogger
Application
hasDatabase
,View
,Session
, andAuthenticator
, andLogger
3.2. Conventional Architecture
A conventional architecture will have each component instantiate its own dependencies. For example, the Application
would do something like this:
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:
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.
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:
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 |