August 2009 - Posts

Service Locator Revisited

I guess I have awakened a slumbering giant by writing the post the other day. I went back to add more functionality to the Service Locator (to enable per transaction and per thread services). Before I started breaking stuff by adding new functionality, I decided to make unit tests for the existing stuff. My first test was simple enough, register and retrieve a service from the ServiceContainer (which might need to be renamed because of a collision with System.ComponentModel.Design.ServiceContainer).

[TestMethod]
public void CanRegisterService()
{
    var dummyService = new DummyService();
    ServiceContainer.RegisterService<IDummyService>(dummyService);
    var retrievedService = ServiceContainer.GetService<IDummyService>();
    Assert.IsInstanceOfType(retrievedService, typeof(IDummyService),"The returned service is not of the correct type");
    Assert.AreEqual(dummyService,retrievedService,"The returned service is a different instance.");
}

Simple enough right? Let’s run it and make sure it’s green and move on…to…hey why is it failing? What happened? After a quick bit of investigation, I found that the problem lay in RegisterService overload that takes the Func:

public static void RegisterService<TService>(Func<TService> creator)
{
    _Container.AddService(creator);
}

Do you see it? Maybe the implementation of AddService might make it more obvious

private void AddService<TService>(TService instance)
{
    _ServicesLock.EnterUpgradeableReadLock();
    try
    { 
        if (!_Services.ContainsKey(typeof(TService))) 
        { 
            _ServicesLock.EnterWriteLock(); 
            try 
            { 
                _Services[typeof(TService)] = instance; 
            } 
            finally 
            { 
                _ServicesLock.ExitWriteLock(); 
            } 
        } 
    } 
    finally 
    {
        _ServicesLock.ExitUpgradeableReadLock(); 
    } 
}

Because of type inference, <TService> is Func<TService> when we try to retrieve it we’re looking for TService as the key, and of course we won’t find it. So there’s a lesson to be learned here. Actually there are two.

  1. It’s the simplest of changes that open the door for the most insidious bugs (not that this bug is very insidious)
  2. If it’s worth writing it’s worth testing. No matter how small the functionality is, you should write a test for it because when it breaks you want to know.

Anyway, now that I’ve got the tests in place for the existing functionality, I’m going to go ahead and see about implementing per transaction life management. But not tonight.

Posted by Mike Brown | with no comments

Service Locator Versus DI – A Clarification

Update:

For some reason Community Server decided that it doesn't want to display the style for the code snippet nor the IFrame for the skydrive link. Here is the final file. Read on for explanation of what it is and what it does.

Browsing though the blog of my friend and co-work, Jon Fuller, I noticed this post on Dependency Injection and Service Location. Especially interesting was this quote:

I was having a discussion with a colleague the other day about DI and Service Location in the context of the question (posed by a third person):

Which DI container/framework should I choose?

His answer:

None; just roll your own and use simple Service Location

It definitely sounded like I’m the colleague in question here. He then goes on to argue against my love of the Service Locator pattern. I think I should clarify what I meant with my statement. For reference, here is my code for a simple service locator. The usage is simple, first I register a service with the Container:

ServiceContainer.RegisterService<ICalculatorService>(new ConcreteCalculatorService());

Then anyone who wants an instance of the ICalculatorService can simply ask for it:

var calc = ServiceLocator.GetService<ICalculatorService>();

This method works for a vast majority of the simple cases where you want to provide a Separation of Concerns. In most cases you don’t need advanced lifetime management (e.g. a single instance of a service will do just fine). The reason I suggested rolling their own was that I assumed from the question the person had no experience with an IOC container prior. It takes time to grok an IOC container, there are a lot of moving parts, and for a simple case, this solution works just fine.

So what happens when I want to allow for more intricate cases. Like different lifecycles for service (per request, per transaction, singleton, etc). Well the choice boils down to extending/enhancing your service locator implementation, or using the Common Service Locator library in conjunction with a container of your choice. You have to decide when the cost of maintaining your own outweighs the benefits. Adding support for a per request lifecycle is simple enough as well however. Here are the changes to the Service Container that supports both a single instance and per request lifecycle:

        private TService RetrieveService<TService>()
        {
            Func<TService> retVal;
            _ServicesLock.EnterReadLock();
            try
            {
                retVal = (Func<TService>)_Services[typeof(TService)];
            }
            catch (Exception)
            {
                throw new Exception(
                   String.Format(
                      "Service Type {0} not registered with container.", typeof(TService).Name));
            }
            finally
            {
                _ServicesLock.ExitReadLock();
            }
            return retVal();
        }

        public static void RegisterService<TService>(Func<TService> creator)
        {
            _Container.AddService(creator);
        }

        public static void RegisterService<TService>(TService instance)
        {
            RegisterService(()=>instance);
        }

I just added an overload to the static RegisterService method that takes a Func that returns an instance of the service, I then change the original method to call the new overload and change the private RetrieveService function to do things properly. To register per request, you now make this simple call

ServiceContainer.RegisterService<ICalculatorService>(()=>new ConcreteCalculatorService());

Using the service is the same as before Now let’s address some of Jon’s concerns regarding the alleged shortcomings of the Service Locator pattern.

Some of his concern stems from the API for the service locator. (Why pass in a separate key when you already have a ready made key in the form of the service type. Also, I’m assuming the “movies.txt” is the parameter for creating an instance of the concrete IMovieFinder. Again, this is not an issue because the instance is created at registration (or using the modified version, through the creator delegate).

With regards to testability, when writing a unit test it’s easy enough to configure a Service Locator to return mocks. Just register the mocks in your setup and call clear on your teardown.

That leaves us with discoverability for consumers of your code. Documentation is a good place to start.  Also, to me putting the dependencies in the constructor opens the door for things like this:

var lister = 
  new MovieLister(
    new CsvMovieFinder(
      new AzureBlobFileRetrieval(
        new AzureKey("account","secretKey"));

True this could be caught in a code review. But it can also be outright prevented by using this great thing called encapsulation. When someone creates the MovieLister that uses the ServiceLocator and debugs, they will get a nice friendly message informing them that they need to register an IMovieFinder with the service container (hopefully they know to do so in the Main() function or wherever your project does it’s container configuration).

To me, there is no major detriment to using either style. Rolling your own involves dealing with maintenance of that solution, but it’s easy enough to swap it out with a third-party container once you have the need for it. Here’s the code for my basic service container. A lot of other scenarios are trivial to implement here as well. And I’ll share them with you in a later post.

Posted by Mike Brown | 2 comment(s)
Filed under: