Is ServiceLocator an anti-pattern? [closed]
Asked Answered
B

10

167

Recently I've read Mark Seemann's article about Service Locator anti-pattern.

Author points out two main reasons why ServiceLocator is an anti-pattern:

  1. API usage issue (which I'm perfectly fine with)
    When class employs a Service locator it is very hard to see its dependencies as, in most cases, class has only one PARAMETERLESS constructor. In contrast with ServiceLocator, DI approach explicitly exposes dependencies via constructor's parameters so dependencies are easily seen in IntelliSense.

  2. Maintenance issue (which puzzles me)
    Consider the following expample

We have a class 'MyType' which employs a Service locator approach:

public class MyType
{
    public void MyMethod()
    {
        var dep1 = Locator.Resolve<IDep1>();
        dep1.DoSomething();
    }
}

Now we want to add another dependency to class 'MyType'

public class MyType
{
    public void MyMethod()
    {
        var dep1 = Locator.Resolve<IDep1>();
        dep1.DoSomething();
            
        // new dependency
        var dep2 = Locator.Resolve<IDep2>();
        dep2.DoSomething();
    }
}

And here is where my misunderstanding starts. The author says:

It becomes a lot harder to tell whether you are introducing a breaking change or not. You need to understand the entire application in which the Service Locator is being used, and the compiler is not going to help you.

But wait a second, if we were using DI approach, we would introduce a dependency with another parameter in constructor (in case of constructor injection). And the problem will be still there. If we may forget to setup ServiceLocator, then we may forget to add a new mapping in our IoC container and DI approach would have the same run-time problem.

Also, author mentioned about unit test difficulties. But, won't we have issues with DI approach? Won't we need to update all tests which were instantiating that class? We will update them to pass a new mocked dependency just to make our test compilable. And I don't see any benefits from that update and time spending.

I'm not trying to defend Service Locator approach. But this misunderstanding makes me think that I'm losing something very important. Could somebody dispel my doubts?

UPDATE (SUMMARY):

The answer for my question "Is Service Locator an anti-pattern" really depends upon the circumstances. And I definitely wouldn't suggest to cross it out from your tool list. It might become very handy when you start dealing with legacy code. If you're lucky enough to be at the very beginning of your project then the DI approach might be a better choice as it has some advantages over Service Locator.

And here are main differences which convinced me to not use Service Locator for my new projects:

  • Most obvious and important: Service Locator hides class dependencies
  • If you are utilizing some IoC container it will likely scan all constructor at startup to validate all dependencies and give you immediate feedback on missing mappings (or wrong configuration); this is not possible if you're using your IoC container as Service Locator

For details read excellent answers which are given below.

Broaden answered 1/4, 2014 at 19:45 Comment(4)
"Won't we need to update all tests which were instantiating that class?" Not necessarily true if you are using a builder in your tests. In this case you would only have to update the builder.Unclinch
You're right, it depends. In large Android apps, for example, so far people have been very reluctant to use DI because of performance concerns on low spec mobile devices. In such cases, you have to find an alternative to still write testable code, and I'd say Service Locator is a good-enough substitute in that case. (Note: things may change for Android when the new Dagger 2.0 DI framework is mature enough.)Despot
Note that since this question has been posted, there is an update on Mark Seemann's Service Locator is an Anti-Pattern post describing how service locator violates OOP by breaking encapsulation, which is his best argument yet (and the underlying reason for all of the symptoms that he used in all prior arguments). Update 2015-10-26: The fundamental problem with Service Locator is that it violates encapsulation.Landlady
Here is an excerpt from DIPP&P that summarizes Service Locator and its main problem.Gastro
H
148

If you define patterns as anti-patterns just because there are some situations where it does not fit, then YES it's an anti pattern. But with that reasoning all patterns would also be anti patterns.

Instead we have to look if there are valid usages of the patterns, and for Service Locator there are several use cases. But let's start by looking at the examples that you have given.

public class MyType
{
    public void MyMethod()
    {
        var dep1 = Locator.Resolve<IDep1>();
        dep1.DoSomething();

        // new dependency
        var dep2 = Locator.Resolve<IDep2>();
        dep2.DoSomething();
    }
}

The maintenance nightmare with that class is that the dependencies are hidden. If you create and use that class:

var myType = new MyType();
myType.MyMethod();

You do not understand that it has dependencies if they are hidden using service location. Now, if we instead use dependency injection:

public class MyType
{
    public MyType(IDep1 dep1, IDep2 dep2)
    {
    }

    public void MyMethod()
    {
        dep1.DoSomething();

        // new dependency
        dep2.DoSomething();
    }
}

You can directly spot the dependencies and cannot use the classes before satisfying them.

In a typical line of business application you should avoid the use of service location for that very reason. It should be the pattern to use when there are no other options.

Is the pattern an anti-pattern?

No.

For instance, inversion of control containers would not work without service location. It's how they resolve the services internally.

But a much better example is ASP.NET MVC and WebApi. What do you think makes the dependency injection possible in the controllers? That's right -- service location.

Your questions

But wait a second, if we were using DI approach, we would introduce a dependency with another parameter in constructor (in case of constructor injection). And the problem will be still there.

There are two more serious problems:

  1. With service location you are also adding another dependency: The service locator.
  2. How do you tell which lifetime the dependencies should have, and how/when they should get cleaned up?

With constructor injection using a container you get that for free.

If we may forget to setup ServiceLocator, then we may forget to add a new mapping in our IoC container and DI approach would have the same run-time problem.

That's true. But with constructor injection you do not have to scan the entire class to figure out which dependencies are missing.

And some better containers also validate all dependencies at startup (by scanning all constructors). So with those containers you get the runtime error directly, and not at some later temporal point.

Also, author mentioned about unit test difficulties. But, won't we have issues with DI approach?

No. As you do not have a dependency to a static service locator. Have you tried to get parallel tests working with static dependencies? It's not fun.

Handicraftsman answered 1/4, 2014 at 20:9 Comment(18)
Jgauffin, thanks for your answer. You pointed one important thing about auto check at startup. I didn't thinking about that and now I see another benefit of DI. You also gave an example: "var myType = new MyType()". But I can't count it as valid one as we never instantiate dependencies in a real app (IoC Container does it for us all the time). I.e: in MVC app we have a controller, which dependents on IMyService and MyServiceImpl depends on IMyRepository. We'll never instantiate MyRepository and MyService. We get instances from Ctor params (like from ServiceLocator) and use them. Don't we?Broaden
Jgauffin, at the end, I will summarize all the good point I was missing and add them as an edit to my question. Your point will be there. Voting up your answerBroaden
@davidoff: I'm waiting on a summary :)Handicraftsman
Your only argument for Service Locator not being an anti-pattern is: "inversion of control containers would not work without service location". This argument however is invalid, since Service Locator is about intentions and not about mechanics, as explained clearly here by Mark Seemann: "A DI container encapsulated in a Composition Root is not a Service Locator - it's an infrastructure component."Gastro
you are wrong. A single composition root would work to resolve controllers. But ASP.NET provide a lot other extension point which also requires services. If you cramp everything into a single place you are providing exactly what SL defines: The service locator pattern is a design pattern used in software development to encapsulate the processes involved in obtaining a service with a strong abstraction layer.Handicraftsman
@Handicraftsman Web API doesn't use Service Location for DI into Controllers. It doesn't do DI at all. What it does is: It gives you the option to create your own ControllerActivator to pass into the Configuration Services. From there, you can create a composition root, whether it be Pure DI, or Containers. Also, you are mixing the use of Service Location, as being a component of your pattern, with the definition of the "Service Locator Pattern." With that definition, Composition Root DI could be considered a "Service Locator Pattern." So the whole point of that definition is moot.Reprimand
I do want to point out that this is a generally good answer, I just commented on one misleading point you made.Reprimand
@Suamere: It's the ControllerBuilder that invokes the ControllerFactory which in turn invokes the ControllerActivator. And the default implementation of the ControllerActivator uses the IDependencyResolver to locate services that should be injected into controllers. You can of course create your own implementation, but that doesn't change the fact.Handicraftsman
I disagree. I precisely use the Service Locator approach because I do not want the client do know about the dependencies. If the client just want to call obj.Method1(), why requiring him to inject your dependencies ? Interface Segregation Principle.Pasto
interface segregation principle is about API design, NOT dependency injection. If you do not want to expose the interfaces you are using, you are rather violating ISP than adhering to it. Small classes following SOLID do not have a problem with large number of dependencies.Handicraftsman
Excellent answer that clarifies my pressing questions on Service Locator disadvantagesMichelle
...Service Location and IoC are different. The "solution" explained here just uses IoC (which is fine) instead of Service Location. Service Location is still bad.Foretopgallant
*DI, not IoC. Service Location and DI are different.Foretopgallant
Inversion of control is a pattern. It's not a tool or a library. An inversion of control container is a library which allows you to use service location. Read the definition in wikipedia of service location.Handicraftsman
@Handicraftsman DI and SL are both version renditions of IoC. SL is just the wrong way to do it. An IoC container might be SL, or it could use DI. It depends on how it's wired. But SL is bad, bad bad. It's a way of hiding the fact that you're tightly coupling everything together.Foretopgallant
Show me one framework (like ASP.NET MVC) which can function without service location. Not just names but how they do it.Handicraftsman
Angular JS, league\container, Spring.NET, Ninject, and many others....Foretopgallant
1. I said Not just names but how they do it 2. Ninject is a container (i.e. a library) and not a framework. You still need to provide an answer.Handicraftsman
S
43

I would also like to point out that IF you are refactoring legacy code that the Service Locator pattern is not only not an anti-pattern, but it is also a practical necessity. No-one is ever going to wave a magic wand over millions of lines of code and suddenly all that code is going to be DI ready. So if you want to start introducing DI to an existing code base it is often the case that you will change things to become DI services slowly, and the code that references these services will often NOT be DI services. Hence THOSE services will need to use the Service Locator in order to get instances of those services that HAVE been converted to use DI.

So when refactoring large legacy applications to start to use DI concepts I would say that not only is Service Locator NOT an anti-pattern, but that it is the only way to gradually apply DI concepts to the code base.

Saree answered 2/4, 2014 at 10:47 Comment(2)
When you're dealing with legacy code, everything is justified to get you out of that mess, even if it means taking intermediate (and imperfect) steps. The Service Locator is such intermediate step. It allows you get out of that hell one step at the time, as long as you remember that a good alternative solution exists that is documented, repeatable and proven to be effective. This alternative solution is Dependency Injection and this is precisely why the Service Locator is still an anti-pattern; properly designed software doesn't use this it.Gastro
RE: "When you're dealing with legacy code, everything is justified to get you out of that mess" Sometimes I wonder if there's only a little bit of legacy code that has ever existed, but because we can justify anything to fix it we somehow never managed to do so.Equivalent
C
9

From the testing point of view, Service Locator is bad. See Misko Hevery's Google Tech Talk nice explanation with code examples http://youtu.be/RlfLCWKxHJ0 starting at minute 8:45. I liked his analogy: if you need $25, ask directly for money rather than giving your wallet from where money will be taken. He also compares Service Locator with a haystack that has the needle you need and knows how to retrieve it. Classes using Service Locator are hard to reuse because of it.

Corissa answered 8/4, 2014 at 14:41 Comment(2)
This is a recycled opinion and would have been better as a comment. Also, I think your (his?) analogy serves to prove that some patterns are more appropriate for some problems over others.Birkett
I think that video was useful enough to warrant a separate answer. Comments are too easily lost.Autophyte
L
8

Maintenance issue (which puzzles me)

There are 2 different reasons why using service locator is bad in this regard.

  1. In your example, you are hard-coding a static reference to the service locator into your class. This tightly couples your class directly to the service locator, which in turns means it won't function without the service locator. Furthermore, your unit tests (and anybody else who uses the class) are also implicitly dependent on the service locator. One thing that has seemed to go unnoticed here is that when using constructor injection you don't need a DI container when unit testing, which simplifies your unit tests (and developers' ability to understand them) considerably. That is the realized unit testing benefit you get from using constructor injection.
  2. As for why constructor Intellisense is important, people here seem to have missed the point entirely. A class is written once, but it may be used in several applications (that is, several DI configurations). Over time, it pays dividends if you can look at the constructor definition to understand a class's dependencies, rather than looking at the (hopefully up-to-date) documentation or, failing that, going back to the original source code (which might not be handy) to determine what a class's dependencies are. The class with the service locator is generally easier to write, but you more than pay the cost of this convenience in ongoing maintenance of the project.

Plain and simple: A class with a service locator in it is more difficult to reuse than one that accepts its dependencies through its constructor.

Consider the case where you need to use a service from LibraryA that its author decided would use ServiceLocatorA and a service from LibraryB whose author decided would use ServiceLocatorB. We have no choice other than using 2 different service locators in our project. How many dependencies need to be configured is a guessing game if we don't have good documentation, source code, or the author on speed dial. Failing these options, we might need to use a decompiler just to figure out what the dependencies are. We may need to configure 2 entirely different service locator APIs, and depending on the design, it may not be possible to simply wrap your existing DI container. It may not be possible at all to share one instance of a dependency between the two libraries. The complexity of the project could even be further compounded if the service locators don't happen to actually reside in the same libraries as the services we need - we are implicitly dragging additional library references into our project.

Now consider the same two services made with constructor injection. Add a reference to LibraryA. Add a reference to LibraryB. Provide the dependencies in your DI configuration (by analyzing what is needed via Intellisense). Done.

Mark Seemann has a StackOverflow answer that clearly illustrates this benefit in graphical form, which not only applies when using a service locator from another library, but also when using foreign defaults in services.

Landlady answered 5/7, 2016 at 11:20 Comment(0)
F
2

My knowledge is not good enough to judge this, but in general, I think if something has a use in a particular situation, it does not necessarily mean it cannot be an anti-pattern. Especially, when you are dealing with 3rd party libraries, you don't have full control on all aspects and you may end up using the not very best solution.

Here is a paragraph from Adaptive Code Via C#:

"Unfortunately, the service locator is sometimes an unavoidable anti-pattern. In some application types— particularly Windows Workflow Foundation— the infrastructure does not lend itself to constructor injection. In these cases, the only alternative is to use a service locator. This is better than not injecting dependencies at all. For all my vitriol against the (anti-) pattern, it is infinitely better than manually constructing dependencies. After all, it still enables those all-important extension points provided by interfaces that allow decorators, adapters, and similar benefits."

-- Hall, Gary McLean. Adaptive Code via C#: Agile coding with design patterns and SOLID principles (Developer Reference) (p. 309). Pearson Education.

Fatimafatimah answered 13/7, 2016 at 14:7 Comment(0)
F
1

I can suggest considering generic approach to avoid the demerits of Service Locator pattern. It allows explicitly declaring class dependencies and substitute mocks and doesn't depend on a particular DI Container. Possible drawbacks of this approach is:

  1. It makes your control classes generic.
  2. It is not easy to override some particular interface.

1 First Declare interface

public interface IResolver<T>
{
    T Resolve();
}
  1. Create ‘flattened’ class with implementing of resolving the most of frequently used interfaces from DI Container and register it. This short example uses Service Locator but before composition root. Alternative way is inject each interface with constructor.
  public class FlattenedServices :
    IResolver<I1>,
    IResolver<I2>,
    IResolver<I3>
  {
    private readonly DIContainer diContainer;

    public FlattenedServices(DIContainer diContainer)
    {
      this.diContainer = diContainer;
    }

    I1 IResolver<I1>.Resolve()
      => diContainer.Resolve<I1>();

    I2 IResolver<I2>.Resolve()
      => diContainer.Resolve<I2>();

    I3 IResolver<I3>.Resolve()
      => diContainer.Resolve<I3>();
  }
  1. Constructor injection on some MyType class should look like
  public class MyType<T> : IResolver<T>
      where T : class, IResolver<I1>, IResolver<I3>      
  {
    T servicesContext;

    public MyType(T servicesContext)
    {
      this.servicesContext = servicesContext
         ?? throw new ArgumentNullException(nameof(serviceContext));
      _ = (servicesContext as IResolver<I1>).Resolve() ?? throw new ArgumentNullException(nameof(I1));
      _ = (servicesContext as IResolver<I3>).Resolve() ?? throw new ArgumentNullException(nameof(I3));
    }

    public void MyMethod()
    {
        var dep1 = ((IResolver<I1>)servicesContext).Resolve();
        dep1.DoSomething();
            
        var dep3 = ((IResolver<I3>)servicesContext).Resolve();
        dep3.DoSomething();
    }

    T IResolver<T>.Resolve() => serviceContext;  
  }

P.S. If you don't need to pass servicesContext further in MyType, you can declare object servicesContext; and make generic only ctor not class.

P.P.S. That FlattenedServices class could be considered as main DI container and a branded container could be considered as supplementary container.

Forbidden answered 25/5, 2022 at 22:9 Comment(0)
J
0

The Author reasons that "the compiler won't help you" - and it is true. When you deign a class, you will want to carefully choose its interface - among other goals to make it as independent as ... as it makes sense.

By having the client accept the reference to a service (to a dependency) through explicit interface, you

  • implicitly get checking, so the compiler "helps".
  • You're also removing the need for the client to know something about the "Locator" or similar mechanisms, so the client is actually more independent.

You're right that DI has its issues / disadvantages, but the mentioned advantages outweigh them by far ... IMO. You're right, that with DI there is a dependency introduced in the interface (constructor) - but this is hopefully the very dependency that you need and that you want to make visible and checkable.

Javier answered 1/4, 2014 at 20:12 Comment(3)
Zrin, thanks for your thoughts. As I understand, with a "proper" DI approach I should not instantiate my dependencies anywhere except unit tests. So, the compiler will help me only with my tests. But as I described in my original question, this "help" with broken tests doesn't gives me anything. Does it?Broaden
The 'static information'/'compile-time checking' argument is a straw man. As @Broaden has pointed out, DI is equally susceptible to runtime errors. I would also add that modern IDEs provide tooltip views of comment/summary information for members, and even in those that don't, someone's still going to be looking at documentation until they just 'know' the API. Documentation is documentation, whether a required constructor parameter or a remark about how to configure a dependency.Sincerity
Thinking about implementation / coding and quality assurance - the readability of code is crucial - especially for interface definition. If you can do without automatic compile time checks and if you do adequately comment / document your interface, then I guess you can at least partially remedy the disadvantages of hidden dependence on a kind of global variable with not easily visible / predictable contents. I'd say you need good reasons to use this pattern that outweigh this disadvantages.Javier
P
0

Yes, service locator is an anti-pattern it violates encapsulation and solid.

Phytogenesis answered 8/10, 2018 at 14:38 Comment(1)
I understand what you mean but it would be helpful to add more detail, e.g. why it violates SOLIDConsociate
A
0

Service Locator(SL)

Service Locator solves [DIP + DI] issue. It allows by interface name satisfy needs. Service Locator can be as singleton or can be passed into constructor.

class A {
  IB ib

  init() {
     ib = ServiceLocator.resolve<IB>();
  }
}

The problem here that it is not clear which exactly classes(realizations of IB) are used by client(A).

proposal - pass parameter explicitly

class A {
  IB ib

  init(ib: IB) {
     self.ib = ib
  }
}

SL vs DI IoC Container(framework)

SL is about saving instances when DI IoC Container(framework) is more about creating instances.

SL works as a PULL command when it retrieves dependencies inside constructor DI IoC Container(framework) works as a PUSH command when it put dependencies into constructor

Astrogeology answered 5/5, 2021 at 13:27 Comment(0)
E
0

I think that the author of the article shot himself on the foot in proving that it's an anti pattern with the update written 5 years later. There, it is said that this is the correct way:

public OrderProcessor(IOrderValidator validator, IOrderShipper shipper)
{
    if (validator == null)
        throw new ArgumentNullException("validator");
    if (shipper == null)
        throw new ArgumentNullException("shipper");
        
    this.validator = validator;
    this.shipper = shipper;
}

Then below is said:

Now it's clear that all three object are required before you can call the Process method; this version of the OrderProcessor class advertises its preconditions via the type system. You can't even compile client code unless you pass arguments to constructor and method (you can pass null, but that's another discussion).

Let me stress the last part again:

you can pass null, but that's another discussion

Why that is another discussion? That is a huge deal. An object that receives its dependencies as arguments fully depends on the previous execution of the application (or tests) to provide those objects as valid references/pointers. It is not "encapsulated" in the terms that the author expressed, as it depends on a lot of external machinery to run in a satisfactory manner for the object to be constructed at all, and later to work properly when it needs to use the other class.

The author claims that it's the Service Locator which is not encapsulated because it's depending on an additional object that you can't isolate from on the tests. But that other object could very well be a trivial map or vector, so it's pure data with no behavior. In C++, for example, containers are not part of the language, so you rely on containers (vectors, hash maps, strings, etc.) for all non-trivial classes. Are they not isolated because rely on containers? I don't think so.

I think that both using manual dependency injection or a service locator the objects are not really isolated from the rest: they need their dependencies, yes or yes, but are provided in a different way. I for one think that a locator even helps with the DRY principle, as it's error prone and repetitive to pass over and over pointers through the application. The service locator can also be more flexible in that an object might retrieve its dependencies when needed (if needed), not only via the constructor.

The problem of the dependencies not being explicit enough via the constructor of the object (on an object using the service locator) is solved by the very thing I stressed out before: passing null pointers. It can be even used to mix and match both systems: if the pointer is null, use the service locator, otherwise, use the pointer. Now it is enforced via the type system, and it's obvious to the user of the class. But we can do even better.

One additional solution that is surely doable with C++ (I don't know about Java/C#, but I suppose it could be done as well), is to write a helper class to be instantiated like LocatorChecker<IOrderValidator, IOrderShipper>. That object could check on it's constructor/destructor that the service locator holds a valid instance of the required classes, hence, being also less repetitive than the example provided by Mark Seeman.

Endymion answered 13/7, 2022 at 21:31 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.