Quicker, Easier, More Seductive: Restraining Your Service Locators
tl;dr: 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.
Quicker, Easier, More Seductive
I mentioned in passing last week, “When it comes to Inversion of Control, a Service Locator is like the Dark Side of the Force: quicker, easier, more seductive. But it gets you into trouble later on. Go with Dependency Injection whenever you can instead.” I want to elaborate on that a bit.
The question is not “Which is better and which is worse, DI container or Service Locator?” The question is “What are the proper limits on DI containers and Service Locators?”
To establish my bona fides here, I’ll point out that I used Service Locator almost exclusively for several years. Solar::dependency() was Solar’s inversion-of-control system, and it was very useful for its purpose. But knowing what I know now, it is much more a Service Locator than a Dependency Injection container. (As a side note, universal constructor was a natural outgrowth of having a Service Locator try to take on Dependency-Injection-like behaviors.)
Service locators are a great boon, and very easy to set up and use. But they also present great problems as systems grow larger. Testing becomes more and more difficult. Dependencies are obscured. If the locator is used via static methods, it’s even harder, and more obscure.
Dependency injection containers take more up-front setup, and more up-front skill and discipline on the part of the developer. They are harder to understand, and it’s harder to get the concepts that go along with DI containers (factories in particular). I’m not kidding when I say it took me a year (!) of reading Misko Hevery and getting input from more experienced DI developers before I began to get it.
It is for that reason I say that Service Locator is “quicker, easier, more seductive.” It’s not that Service Locator is bad, it’s that it gets out of control very quickly.
Restraining Your Service Locators
Service Locator still has a useful purpose, but its scope must be limited to make sure it does not grow into a monster. Other people have suggested various rules on when to use a Service Locator, and when to use a Dependency Injection container. I will not add to those rules.
Instead, I will tell you to go ahead and inject a Service Locator (no static calls!) but on one condition:
A Service Locator should contain only one type of object.
When I say “type” I don’t mean “anything my controller might happen to need.” I mean, loosely speaking, “one class of object” or “objects with the same purpose”. For example, the Aura filter system uses a locator to make different validation and sanitizing rules available. The v1 view package uses a locator for the helper objects; we have gone so far as to extract the locator and helpers to their own package in v2. Note that these locators are highly specific and handle only one type of object.
-
What if you need multiple types of objects? Inject multiple types of Service Locators.
-
What if you only need one each of several different objects? Inject those objects directly, probably via constructor arguments, instead of retrieving them from a Service Locator.
-
What if some of the objects are optional, or only needed some of the time? Inject them via setter methods.
-
What if it ends up that you genuinely need to inject lots of different locators? Take it as a sign that you need to refactor the object that needs the many different locators. Consider wrapping those parts of the object into a service, inject the locators to the service, and inject the service to the original object.
-
If you find yourself injecting a Dependency Injection container into an object so that the object can retrieve dependencies from the container, you are using the Dependency Injection container as a Service Locator. The "only one type of object" rule applies in that case. The only way around it is to stop injecting the Dependency Injection container and instead inject a particular Service Locator, or to inject the specific needed objects.
The condition of “only one type of object” is imperfect, but it should begin set you on the correct path; that is, toward being able to identify dependencies. The goal is not immediate perfection, but gradual improvement. The idea is to purposely make Service Locator a little less seductive, to add constraints to it, so that it does not become a menace later.
Afterword
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.