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:
- Aura.Di: DI Container
-
Laravel Container:
Service LocatorDI container - Pimple (used by Silex etc): Service Locator
-
Slim (via
$app
): Service Locator - Symfony DependencyInjection component: Service Locator (?!)
- Zend Di: DI Container
Some notes:
-
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
andnewInstance()
instead of$abstract
andmake()
would make things more obvious IMO) it does fulfill the above heuristic. -
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.
-
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. -
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.
Read the Reddit discussion about this post here.