Quicker, Easier, More Seductive: How To Tell A DI Container From A Service Locator

(In this unexpected series, we’ve been talking about the use of Service Locator versus Dependency Injection.)

UPDATE: Please see the next post in this series for important corrections and updates regarding the essay below.


It is easy to confuse a Dependency Injection container with a Service Locator. They are very similar to each other. The differences are subtle. Indeed, it’s even possible to use a Dependency Injection container as a Service Locator, although I think it’s difficult to use a Service Locator as a Dependency Injection container. They are both sub-patterns of a more generic pattern called Inversion of Contol, and some people confuse the term Dependency Injection with the more generic term Inversion of Control.

How To Tell The Difference

I’ve been thinking about what heuristic or rule-of-thumb one could use to easily tell which is which. So far I’ve been leaning in the direction of: “Can you create new instances without defining a service?” If you can, it’s a Dependency Injection container; otherwise, it’s a Service Locator. There are several variations on that prhasing:

  • If you can create a new instance of a class without having to define a service, it’s a Dependency Injection container.

  • If you have to define a service in order to get an object out of the container, it’s a Service Locator.

  • If you cannot create a new instance of a class without defining it as a service, it’s a Service Locator.

By this I mean that you should be able to repeatedly create new, discrete, independent instances of a class, without having to define it as a service in the container.

Examples

Given the above heuristic, here’s an example of Dependency Injection using Aura.Di:

// they are separate instances, not shared services
$foo_1 = $di->newInstance('FooBarBaz');
$foo_2 = $di->newInstance('FooBarBaz');
var_dump($foo_1 === $foo_2); // false

Might you need to set up constructor parameters, setters, etc. for the newInstance() calls? Sure; Aura.Di allows for inherited and overridable constructor parameters and setter methods:

// given __construct($gir)
$di->params['FooBarBaz']['gir'] = $di->newInstance('DibZimGir');

// given a setter method setOperation($operation_name)
$di->setter['FooBarBaz']['setOperation'] = 'ImpendingDoom';

The main point is that whenever the Dependency Injection container creates a class, it can do so without having to define that class as a reusable service.

In contrast, a Service Locator requires that you define a service, and then you can retrieve a shared instance of that service from the container. Here’s an example with my trivial Service Locator example from earlier in this series:

// define shared services
$locator->set('gir', function () use ($locator) {
    return new DibZimGir;
}

$locator->set('foo', function () use ($locator) {
    $foo = new FooBarBaz($locator->get('gir'));
    $foo->setOperation('ImpendingDoom');
    return $foo;
});

// these are shared services, not separate instances
$foo_1 = $locator->get('foo');
$foo_2 = $locator->get('foo');
var_dump($foo_1 === $foo_2); // true

Who’s Doing What?

I had a quick look at some containers in PHP land to see how well they fulfill the above heuristic; my assessment is as follows:

Some notes:

  1. Laravel: The documentation calls the container an “IoC” (Inversion of Control) container but mentions “Dependency Injection” throughout, never “Service Locator.” It is clear from the container’s usage throughout the rest of the codebase that it is in fact a Service Locator, since the container itself is injected into the classes that need services. This strikes me something that can be remedied with a documenation change, since the class itself is named in a generic way. UPDATE: the project lead has informed me of the public make() method on the container that creates new instances. Although I pick nits over the naming (using $class and newInstance() instead of $abstract and make() would make things more obvious IMO) it does fulfill the above heuristic.

  2. Pimple: Interestingly, Pimple has a factory() method that will return independent instance of a service. Some may think of this as meeting the heuristic above, but I don’t think it does; you still have to define the service, after all. I suppose one might define the service as a factory for a particular class name, and name the service for the class to be factoried, but that’s not quite the same; among other things, you still have to define a service for each class you want to factory.

  3. Slim: This uses $app as a Service Locator. To his credit, Josh Lockhart does not call it either a Service Locator or a Dependency Injection container, so kudos to @codeguy for avoiding the issue altogether.

  4. Symfony DependencyInjection component: My quick review of the codebase did not reveal a way to create a new instance of a class. Instead of a missing capability, It seems more likely to me that I just did not see it. If there are any Symfony guys reading this, please let me know how to create a new instance of a class with the Symfony DependencyInjection component, and I’ll update the description.

Conclusion

According to the heuristic above, some of the things calling themselves Dependency Injection systems are probably better described as Service Locators. They might be Containers (or Inversion of Control containers) in a generic sense, but they are not Dependency Injection containers. There’s nothing wrong with not-being a DI container, but using the wrong names for things leads to confusion on a topic filled with subtleties.

Of course, I’m sure that the folks using these Service Locator systems will have plently of rationalizations for why (1) the preferred system of their particular tribe really is a Dependency Injection container no matter what made-up rules Paul wants to apply, and (2) the distinction doesn’t really matter anyway, why even bring it up, just use their preferred system. I look forward to the comments on this one. ;-)

Afterword

Are you overwhelmed by a legacy PHP codebase? Do you want to improve it, but don’t know where to start? My book, Modernizing Legacy Applications in PHP, will lead you step-by-step through a series of small, incremental changes that will dramatically improve the quality of your legacy codebase.

12 thoughts on “Quicker, Easier, More Seductive: How To Tell A DI Container From A Service Locator

  1. Not sure if that’s what you’re looking for, but in Symfony2 DIC you can set the scope to prototype and a new instance will be created every time the service is injected someplace.

    I wonder how you’ve arrived at that rule though? Could you go more in depth how that is a defining characteristic of a DIC?

  2. Defining a service with

    scope: prototype

    yields a new instance each time you request a service from the symfony DIC.

  3. To be honest, I’m still confused.

    I understand that DI can be done through the constructor & getter/setter methods. Also that a Service Locator (usually injected via the constructor or a setter method) is an object that knows everything I may need in my object and said object can use it to pull those things already instantiated (via factory, or just instantiating the object).

    Where does the DIC come in? Is this something that is more specifici to framework/library code than app code?

    In my apps I tend to use (probably over use) a service locator that knows how to instantiate the objects that perform the business logic. Usually those have few dependencies, mostly the data mapper(s) and perhaps another object that performs actions on related data. So the only thing they get injected with at instantiation is the service locator to pull out the data mapper & the other object/service the one object may need. That’s if I’m using one of the larger frameworks (Zend/Symfony).

    If I’m doing a small project (using Slim or Silex) I typically don’t have very many objects and then I just instantiate them before the $app gets run and inject them into the app.

    So I don’t see where the DIC would come in a way that would make it different from a Service Locator. Then again, I’m still learning most of this and perhaps I’m just doing things wrong :)

  4. From a practical point of view I don’t see the difference. The fact that you can do $dic->newInstance($class) is irelevant for complex operations because the DiC should be able to construct a complex object. If $class depends on 3 other objects one being a singleton, another a new instance of a class and another being an object being a service registered within the DiC/Service locator you would need to register the object you want to get as a service. I don’t see a DiC having a newInstance() method capable of handling the example above without a performance hit/extra configuration work.

    The fact that the only thing that a ServiceLocator would need to become a DiC is a newInstance() method for simple classes makes the whole “fight” over the terminology unjustified.

    Maybe an “real-life” for the need of having newInstance() would help me see what I’m missing. For the moment I’m thinking of a data-mappper that must map the result of an SQL query onto a collection of objects, each object having to be a new instance of a class. Zend solved this problem by using a setResultsetPrototype($object) method and the mapping of the result is done on a clone of that object, so the importance of $dic->newInstance() still eludes me.

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>