Quicker, Easier, More Seductive: The Difference Between Factories, Registries, and Service Locators
In last week’s episode of this unexpected series, I said, “Go ahead and use Service Locator, but make sure each Locator contains only one type of object. This condition of restraint will keep a single locator from becoming a menace.” (I recommend “real” dependency injection containers as the preferred inversion-of-control mechanism; try the Aura.Di container.)
The “only one type of object” constraint generated some discussion regarding factories and registries. On Reddit, teresko commented:
I don’t think I have ever seen a use of Service Locator where it adhered to this rule. Hell … I am not even sure that you can call it “Service Locator” then, because it is just a smarter sounding name for Registry pattern.
When you apply this rule, what you get instead is a factory, which enforces uniqueness of an instances, that it creates.
On the blog itself, Adrian Mu asked:
In the case of the Aura’s filter package am I wrong to interpret the “rule service locator” as a “rule factory”? If no, what is the difference between a factory and a service locator as you describe it? If yes, where?
And on Twitter, Taylor Otwell asked a similar question:
that “one type” of service locator is just a “factory”, right?
The differences between factories, registries, and containers (both service locators and dependency injection containers) can be subtle and hard to grasp at first. Here’s how I keep them straight:
-
a Factory creates and returns an object, but does not retain the created object for future use;
-
a Registry retains an object instance by name for repeated use, but does not create that object;
-
a Service Locator contains a named object (a “service”) and creates it if it
has a definition for that service; the creation logic might itself be a Factory, and the retention logic might itself be a Registry.
By way of example, I have put together an example service locator implementation along with an embedded test case. It has only two methods: set()
and get()
:
-
The
set()
method takes an object name as its first argument, and
a factory (in this case, a callable). The factories are retained in a registry (in this case, a plain-old PHP array). -
The
get()
method looks in a registry of instances for the requested object by name and returns it. If the object is not there, it uses the factory for that object name and retains it in the instance registry.
Here’s an example use:
<?php
// create the service locator
$locator = new PmjonesServiceLocator;
// add a factory; this can be any callable, whether
// a closure, an object with an __invoke() method,
// a function name, a static class method, an object
// instance and method name, etc.
$locator->set('my_object', function () {
return new VendorPackageClassName;
});
// get back an object instance; the locator
// will create and retain it the first time
$one = $locator->get('my_object');
// the next and subsequent times, the locator
// return the same instance
$two = $locator->get('my_object');
var_dump($one === $two); // true
?>
That should help illustrate the difference between a factory, a registry, and a service locator.
Again, I must stress: Service locators can easily get out of control. If you can avoid using a service locator, you should. Instead of service locators, use dependency injection proper as much as possible, even though it is more work up front. If you must use a service locator: place only one type of object in it, use multiple locators if necessary, and inject each locator instead of using static calls for access.
Afterword
In Aura, we use dependency injection everywhere, all the time. In the rare cases that a service locator makes sense, that locator follows the “only one type of object” rule, and the locator itself is injected via the DI container. This makes the codebase very clean and a lot easier to test.
If you like clean code, fully decoupled libraries, and truly independent packages, then the Aura project is for you. Download a single package and start using it in your project today, with no added dependencies.
Read the Reddit discussion about this post here.