Service Locator Pattern or Anti-Pattern?

Service Locator Pattern or Anti-Pattern?

For a long time now I have considered the Service Locator an Anti-Pattern, but recently I have come to question this resolve, is it black and white? Maybe not.

Mark Seemann first introduced me to the idea of an Anti-Pattern in general with his article Service Locator is an Anti-Pattern, I was younger and much more impressionable back then and took it at face value. My older and wiser self (please note being wiser does not mean that I am that wise ;) reflects a bit more on points of view before just accepting them. I don't often reconsider my position on preconceived beliefs, but recently a friend of mine aussieviking prompted me to think about this Pattern again, and there might be a little bit of room in my stubbornness to give it some love.

So first why would I even subscribe to this point of view? Well, it is a valid one, when you use a service locator all you are doing is making life more difficult for your future self or the next poor dev to work on the code base. It causes class dependencies to become fuzzy and is downright frustrating for consumers of that class, especially when you don't have access to the source code. Have a read of Mark's article above for some solid points, as explaining why you should consider it an Anti-Patten is not the focus of my brain dump today.

At some point as part of your application's execution, you need to do some form of service location, and this is, in most cases, reserved for application bootstrapping. Here you ask the container to resolve the applications object graph, from this point onward asking the container for a dependency is, for the most part, what is considered the Service Locator (Anti)Patten. When using IoC in conjunction with MVC, the controller factory will most likely be carrying out this task with a call to your container asking for an instance, Container.GetInstance(controllerType) in the case of SimpleInjector. Using the container in this way should not be confused with the use of the Service Locator Pattern, it is an adaptor between your container and the MVC framework and considered as part of the bootstrapping of your controllers.

Let's imagine you want to build a scheduled task that is managed by a third-party framework, but there is no way to control the construction of it. Having no control over the object graphs resolution screams, ‘Well I can just access the container to get at the dependencies with this Service Locator Patten thing then!’ You, however, need to ignore this voice and introduce the idea of an adaptor instead. As Sitecore scheduled tasks fit this scenario, let’s build an adaptor to solve this problem.

First, we need to explore how schedule tasks work in Sitecore, and before that, we need to know how to define a task. There are so many posts already out there I won't be rehashing how to create one, if you don't, head over the the Sitecore Community website and check out this post by John West.

In the Sitecore.Processing.config include you should find the /configuration/Sitecore/scheduling element; here you see some of the default scheduling agents that ship with Sitecore, we are only interested in one for this post the Master_Database_Agent. This agent is responsible for checking the content tree for schedule items, the logic for this is:

  • Find all the items under the scheduled path that inherit from the Schedule Task Template.
  • Checks to see if the task should run.
  • If it should run, then the referenced command is examined.
  • The command has two fields, Type and Method. These fields are used by Sitecore reflection to create an instance of this type and then invoke a method.

So with the above in mind, we can "hijack" the logic by using one of my favourite things about the C# language, generics.

Define a generically typed class SomeClass<TCommand> and remember that the Sitecore reflection utility is used to create an instance based off the string defined in the command item, because of this we can craft a string that allows the creation of a typed instance of our class.

ServiceLocatorPatternOrAntiPattern.ScheduleTasks.ScheduleTaskContainerAdapter`1[ServiceLocatorPatternOrAntiPattern.ScheduleTasks.ExampleScheduleTask], ServiceLocatorPatternOrAntiPattern

With this knowledge, we create a generically typed Command that is a container adaptor, asking the container to resolve the type parameter and calling the Execute method supplying it with the arguments passed from Sitecore.

public class CommandTaskContainerAdapter<TCommand>
    where TCommand : ICommand
    {
        public void Execute(Item[] items, CommandItem commandItem, ScheduleItem schedule)
        {
            var scheduleTask = (ICommand)SimpleInjectorBootstrapper.Container.GetInstance(typeof(TCommand));
            scheduleTask.Execute(items, commandItem, schedule);
        }
    }

We could take this a step further and remove the typed generics by adding an extra field to the command item. You can then set the existing Type and field to a non-generic command container adaptor, and the new field references the type of your command.

 public class CommandTaskContainerAdapter
    {
        public void Execute(Item[] items, CommandItem commandItem, ScheduleItem schedule)
        {
            var commandTypecommand = commandItem["Command Type"];
            var commandType = Type.GetType(commandTypecommand);
            var command = (ICommand)SimpleInjectorBootstrapper.Container.GetInstance(commandType);
            command.Execute(items, commandItem, schedule);
        }
    }

Personally, I prefer the generic approach as you don’t have to alter the fields on the command template or create a new one, but the sample project that goes along with this post has examples for both options.

In conclusion, is using a Service Locator ok? Well, no not really. However, it can be appropriate to create the adaptors discussed in this post, only in cases where you do not control the instantiation of an object, keeping in mind that it's only responsibility is to resolve your object graph, and not be tempted to use the container again.