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.
Dim conn As SqlConnection = new SqlConnection()
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.
Dim conn As IDbConnection = new SqlConnection()
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:
Dim conn As IDbConnection = CreateConnection() ... Function CreateConnection() as IDbConnection ' create and return an IDbConnection implementation End Function
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.
'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
The following class is my standard implementation. It doesn’t do much – it just wraps the HttpContext session object.
'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
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.
'IServiceLocator.vb Public Interface IServiceLocator Function Resolve(Of T)() As T End Interface
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.
'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
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:
'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
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:
HttpContext.Current.Session.Item("username") = "Grant"
And following is the revised code, using the Service Locator:
ServiceLocator.Resolve(Of ISession)().Item("username") = "Grant"
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.
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:
'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
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.
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:
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
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:
<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
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.
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
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.
Here some additional articles that are worth reading to gain a deeper understanding of the service locator.
- Inversion of Control Containers and the Dependency Injection pattern – Martin Fowler mentions the service locator and how it relates to the other two techniques
- IServiceLocator a step toward IoC container / Service locator detente – Glenn Block writes on a recently created library that provides a common abstraction and adapters for various .NET inversion of control libraries
- CommonServiceLocator – The project mentioned above