Introduction

The previous article in the series has set the stage for this series of reducing code coupling. This article starts getting into the meat of the series by discussing the Service Locator.

About the Service Locator

In code, objects are typically instantiated via the new operator. Doing this couples a variable to whatever type was provided to the new operator. In a lot of cases, this is the normal thing to do. But in some situations, this can lead to undesirable higher coupling. An example is shown below, where a database connection is created.

[code lang="vb"]
Dim conn As SqlConnection = new SqlConnection()
[/code]

Note the direct coupling to the SqlConnection type, and the creation of an object of the type. Any following code that uses the variable will also be coupled to this type. Now look at the next sample.

[code lang="vb"]
Dim conn As IDbConnection = new SqlConnection()
[/code]

A slight change here, the variable type is an interface, IDbConnection. And of course, the variable is assigned an object of type SqlConnection, which implements the interface. Even though the variable is coupled to the interface, the whole line is still tightly coupled since it has to know about the SqlConnection implementation. Consider the further evolution of the sample:

[code lang="vb"]
Dim conn As IDbConnection = CreateConnection()
...
Function CreateConnection() as IDbConnection
' create and return an IDbConnection implementation
End Function
[/code]

The variable is coupled only to the IDbConnection interface, and receives an implementation from the CreateConnection function. The variable does not know what the implementation is, only that it implements the interface. That is programming to interfaces. Also note that by delegating the construction work to a separate function, the code sample shows a very simple example of service location. In this case, receiving dependencies from elsewhere is preferable to creating them directly; this can help reduce coupling.

The Service Locator provides a central location to store common dependencies, essentially a registry, and a way to retrieve necessary dependencies on demand. By encouraging the practice of programming to interfaces, use of the service locator can lead to reduced coupling in code. A nice side effect of reduced coupling is testable code, which will be shown as well.

Service Locator in a Real-World Example

If the above overview has you thinking, “Huh?”, then no worries: I have a real-world example ready for you. My previous programming articles have been based around my ongoing legacy project, writing as I refactor and update, and there will be no change here.

You will see how I have implemented and used the service locator in my project to eliminate some coupling, thereby making the code easier to change and test.

The Starting Point

The application, in its internal workings, stores some data in the session, and retrieves the data later. This was achieved by referencing the Session object. This is all well and good for the most part, but what if session data should be stored elsewhere, perhaps in a database? The standard session store (HttpContext.Current.Session) keeps session data in memory on the server. Another concern is testability: the standard session object is active on a website, not when running unit tests. One would want to replace the standard session store to simplify testing.

The interface shown below is how I define typical session data storage and retrieval.

[code lang="vb"]
'ISession.vb
Public Interface ISession
Property Item(ByVal key As String)
Sub Remove(ByVal key As String)
Function Contains(ByVal key As String) As Boolean
Sub Clear()
End Interface
[/code]

The following class is my standard implementation. It doesn’t do much – it just wraps the HttpContext session object.

[code lang="vb"]
'Session.vb
Public NotInheritable Class Session
Implements ISession

Public Property Item(ByVal key As String) As Object Implements ISession.Item
Get
Return HttpContext.Current.Session.Item(key)
End Get
Set (ByVal value As Object)
HttpContext.Current.Session.Item(key) = value
End Set
End Property

Public Sub Remove(ByVal key As String) Implements ISession.Remove
HttpContext.Current.Session.Remove(key)
End Sub

Public Function Contains(ByVal key As String) As Boolean Implements ISession.Contains
Return HttpContext.Current.Session(key) Is Nothing
End Function

Public Sub Clear() Implements ISessionStore.Clear
HttpContext.Current.Session.Clear()
End Sub
End Class
[/code]

Here I have a simple interface and a simple implementation. I now need service locator functionality to store the implementation, and provide it when needed. That is coming up next.

A Simple Service Locator

Following is an interface that defines what the service locator does, and how to interact with it. Note that the interface is a minimal one – this is for a reason that will soon be elaborated.

[code lang="vb"]
'IServiceLocator.vb
Public Interface IServiceLocator
Function Resolve(Of T)() As T
End Interface
[/code]

Shown below is a simple service locator implementation. Note that in addition to implementing the interface, it adds some additional functionality. The extra method is for adding services to the registry – this is not relevant to the interface, as a class consuming the interface only needs to know how to retrieve services. This extra functionality is within the implementation, as different implementations may work in different ways so far as registering services. But retrieval tends to be fairly standard.

[code lang="vb"]
'ServiceLocatorBasic.vb
Public NotInheritable Class ServiceLocatorBasic
Implements IServiceLocator

Protected items As IDictionary(Of Type, Type)

Public Sub New()
items = New Dictionary(Of Type, Type)
End Sub

Public Sub Register(Of Serv, Impl)()
items.Add(GetType(Serv), GetType(Impl))
End Sub

Public Function Resolve(Of T)() As T Implements IServiceLocator.Resolve
If Not items.ContainsKey(GetType(T)) Then
Return Nothing
End If

Return CType(Activator.CreateInstance(items(GetType(T))), T)
End Function
End Class
[/code]

This is a very basic implementation, but it serves the purpose for this article. One issue to consider is whether it would be more appropriate to throw an exception when resolving an unregistered service. Also, this implementation does not handle the case where a registered implementation may have construction dependencies of it’s own. There are other points to consider, but they are not strictly relevant here and now.

With the interface defined, and an implementation created, there remains the issue of how calling code will interact with the service locator. There are different ways to store the service locator and access its functionality. One is as a singleton, where the implementation can be retrieved a Current method. Another is as an application variable. I think the one that makes the most sense is as shown below:

[code lang="vb"]
'ServiceLocator.vb
Public NotInheritable Class ServiceLocator
Private Shared _locator As IServiceLocator

Public Shared Sub Initialize(ByVal serviceLocator As IServiceLocator)
_locator = serviceLocator
End Sub

Public Shared Function Resolve(Of T)() As T
Return _locator.Resolve(Of T)()
End Function
End Class
[/code]

As the code shows, the class has an internal IServiceLocator member. It has a function that mirrors that of the ServiceLocatorBasic class, namely the Resolve function, which just passes through to the internal implementation. There is also an initialize method, which receives and stores an IServiceLocator implementation. This class does not know what the implementation is, nor does it need to know. This function will be further discussed in the Registering Services section.

This resembles the Gateway design pattern, where a class passes function calls and parameters to an internally-held implementation, insulating consuming code from the implementation. A gateway-type class is usually not static though, and has instance methods. Seeing as this class is made up of shared (static) functions, this could be called a Static Gateway.

Using the Service Locator

Continuing with the ISession example, the direct coupling to the HttpContext session can be eliminated by retrieving the session store implementation from the Service Locator. Below is what was in place previously:

[code lang="vb"]
HttpContext.Current.Session.Item("username") = "Grant"
[/code]

And following is the revised code, using the Service Locator:

[code lang="vb"]
ServiceLocator.Resolve(Of ISession)().Item("username") = "Grant"
[/code]

This code is no longer using the standard session store (that it knows of, anyway), just the ISession interface. And that is all that is needed. The calling code knows what the implementation is capable of, by virtue of the interface.

Registering Services

As mentioned before, the IServiceLocator interface is a minimal one, as code consuming the interface only needs to retrieve services. As also mentioned previously, the registration functionality belongs in the classes implementing the interface.

Registration of services only needs to be done in one location: the application’s start up routine. This is also the only location that needs to know which implementation of the IServiceLocator interface is used.

If the application is a web application, the initialization code can go in the Application_Start sub of the Global.asax file. Essentially what is done is that an implementation is created, initialized by registering services, and finally given to the ServiceLocator class (which is receiving an implementation of IServiceLocator – that is all it needs to know). This process is shown by the following code:

[code lang="vb"]
'Global.asax.vb
Sub Application_Start(ByVal sender As Object, ByVal e As EventArgs)
Dim locator As New ServiceLocatorBasic()
locator.Register(Of ISession, Session)()
'register any other necessary services

ServiceLocator.Initialize(locator)
End Sub
[/code]

After this initialization has completed, the rest of the application is able to interact with the service locator and retrieve services as required, as shown above.

Testing

As mentioned before, use of the standard session was peppered around the project. Those were replaced easily enough by updating those lines to go through the service locator, as shown previously. All well and good for production code. But it’s now time to visit the concern of testability. One of the rules of a unit test is that it should not use any external services, such as the database, filesystem, or network. This includes the web server, where the standard session store is located. To properly unit test code that uses the session storage feature, the code must use a fake session store, such as the following:

[code lang="vb"]
Public Class SessionDummy
Implements ISession

Private _store As IDictionary(Of String, Object)

Public Sub New()
_store = New Dictionary(Of String, Object)
End Sub

Public Property Item(ByVal key As String) As Object Implements ISession.Item
Get
Return _store.Item(key)
End Get
Set (ByVal value As Object)
_store.Item(key) = value
End Set
End Property

Public Sub Remove(ByVal key As String) Implements ISession.Remove
_store.Remove(key)
End Sub

Public Sub Clear() Implements ISession.Clear
_store.Clear()
End Sub

Public Function Contains(ByVal key As String) As Boolean Implements ISession.Contains
Return _store.ContainsKey(key)
End Function
End Class
[/code]

This class is a simple implementation of the ISession interface, and uses a Dictionary to hold the data. More or less what the standard session does already, but anyway…It is not the session, it is not on the web server, and that is what matters. And I know the dummy bit may not be the most appropriate here. The semantics of fake, dummy, mock etc are best left for another article :)

Previously shown was the way in which the service locator is initialized and populated with services, for a production scenario. For setting up a unit test, the registration needs to be done a little differently, as shown by the following test fixture:

[code lang="vb"]
<testFixture()> _
Public Class AUnitTestFixture

<setUp()> _
Public Sub TestSetUp
Dim locator As New ServiceLocatorBasic()
locator.Register(Of ISession, SessionDummy) 'register the dummy version of the session for use by code under test
'register any other dummy services

ServiceLocator.Initialize(locator)
End Sub

<test()> _
Public Sub AUnitTestCase
'code under test will get an ISession, the dummy implementation
End Sub
End Class
[/code]

The end result: a “session” object is available for use, no web server required, and no code breaks because of missing session functionality. Obviously, a little extra work is involved here, but it allows code to become more easily testable by controlling the external dependencies.

A particular downside here is that you have to know what services are used by the code under test, so you can provide the appropriate fake versions. This means you need to be well familiar with not only the code being tested, but any other code that is used which may require a service. This can potentially get complicated.

Comments

Having been through all this talk and code, I’ve got a brief collection of impressions gained while implementing the service locator in my project.

  • It separates construction of dependencies from usage of same, and encourages abstraction through programming to interfaces
  • Adds some complexity by having to go through the Service Locator
  • Provides a single location to manage commonly used dependencies
  • Can make tests easier to set up by providing fake versions of dependencies, so code under test will not break when real dependencies are unavailable
  • Having to set up fake dependencies can add complexity to tests, so the testing aspect is a double-edged sword
  • Also requires some additional configuration and registration for production code
  • Want to minimize use of service locator where possible – this can be done by wrapping with factory or gateway classes
  • Can use basic implementation as shown, or implement a wrapper for one of the IoC libraries and use that for service location

Conclusion

I’ve given a broad understanding of what a service locator is, and the purpose it serves. The following discussion showed how the technique can be implemented in an existing code base, and the advantages it brings for reduced coupling and increased testability. I also pointed out some disadvantages that come with the benefits.

The service locator technique is useful in some situations, and it does help to reduce coupling. However, it brings some disadvantages of its own, including, ironically, introducing some coupling in order to reduce other coupling. I think that on balance, using a Service Locator brings us ahead in reducing coupling and increasing testability.

This was a discussion of just one technique; the next article will cover another, known as Dependency Injection.

Additional Reading

Here some additional articles that are worth reading to gain a deeper understanding of the service locator.