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.

ServiceLocator is like the Dark Side: Quicker, Easier, More Seductive

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.

A lot of things calling themselves Dependency Injection containers are actually Service Locators. If you inject the container into an object, or if you call it statically from inside an object, and the object then pulls things out of the container, the container is acting as a Service Locator.

A Peek At Aura v2 — Aura.Router

Most routing systems combine the “routing” task with the “dispatch” task. That is, they both extract the parameters and pick the controller/action. Aura.Router v2, because of its truly independent nature, only does routing. It turns out that dispatching is something that can be independent of routing, and so we have a separate Aura.Dispatcher package to handle that (although you can use any dispatching system you like).

Aura.Router v2 allows you to examine the $_SERVER values and pick a route based on them using the addServer() method. For example, if you want to match routes based on the HTTP_ACCEPT value …

<?php
$router->addRoute('json_only', '/accept/json/{id}')
->addServer(array(
  // must be of quality *, 1.0, or 0.1-0.9
  'HTTP_ACCEPT' => 'application/json(;q=(*|1.0|[0.[1-9]]))?'
));
?>

Sometimes we need a params in the route path to be sequentially optional. The classic example is a blog archive organized by year, month, and day. We don’t want to have to write three routes, one for /{year}, /{year}/{month}, and /{year}/{month}/{day}, each with repeated information about the route.

In Aura.Router v2, there is a special notation similar to URI Templates that indicates sequentially optional params: {/param1,param2,param3}

We can now add REST resource routes in a single call to attachResource():

<?php
$router->attachResource('blog', '/blog');
?>

This will add seven REST routes with appropriate names, paths, HTTP methods, and token regexes …

If you decide those routes are not to your liking, you can override the default behavior by using setResourceCallable() to pass callable of your own to create resource routes.

Via A Peek At Aura v2 — Aura.Router.

If you respect a [developer], you talk about the [code]; if not, you psychoanalyze the [developer].

… if you respect a writer, then you talk about the work. If you disdain the writer, then you try to psychoanalyze the writer and figure out why would he write this. … If they mention my work at all, which they rarely do, it’s to dismiss it and to psychoanalyze me, which they are incapable of doing … They have no idea what I’m talking about.

A lot of criticism is like, including project criticism. Via Critics, community and ‘Ender’s Game’: An interview with Orson Scott Card | Deseret News.

A Peek At Aura v2: Aura.Web

… it turns out extracting Aura.Dispatcher was the key to reducing the Aura.Web package contents. With Aura.Dispatcher, any object can be a controller, since it can dispatch to any method on any object (as well as dispatching to closures). In turn, there is no more need for the Aura.Web package to provide a base controller with interfaces for various implementations. Instead, you can dispatch to any object you like, with the dependencies you prefer, as your controller. That controller object can be implemented at the framework level, or in a separate package or bundle.

As a result, this means that web-specific request and response objects can be in their own package independent of any particular controller implementation. The Aura.Web v2 package contains only those request and response objects.

via A Peek At Aura v2 — Aura.Web.

A Peek At Aura v2: Aura.Dispatcher

It turns out that dispatching at the framework level, and at the web and CLI levels, is all remarkably similar. After realizing that, we extracted the dispatching logic to its own independent package, without any dependendcies on any other packages.

The Aura.Dispatcher package lets you define named objects that will get instantiated only as they are dispatched to (i.e., lazy loading). It picks which named object to instantiate, and optionally which method to invoke, based on an array of router values (or any other array you wish to pass). It also works as a micro-framework dispatcher; instead of using an object factory proper, you can add a named closure and that will be invoked.

Additionally, if you want a two-stage invocation where the dispatcher picks an object, and the object picks its own method, Aura.Dispatcher comes with a trait that lets you pass named parameters to any method you like. You can use that trait in any object to pick a method and invoke it with the router (or other) parameters.

via A Peek At Aura v2 — Aura.Dispatcher.

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.