DI, DiC, & Service Locator Redux

October 10th, 2012 § 17 comments § permalink

To DiC, or not to DiC: that has seemed to be the question in PHP for the last few years. Most people generally agree that injecting dependencies is the right thing to do®. For those writing a framework, or any shared codebase where extensibility or the ability to grow the codebase is a core philosophical tenet, not injecting dependencies is doing a disservice to the project in the long run. So, as I’ve stated before, the question becomes how do we manage the added complexity that comes with practicing dependency injection?

Recapping What a DiC Is

A DiC (dependency injection container) is a special type of object container that acts as one part object registry, one part object factory. In most common and popular implementations, the container is seeded with or has access to data about what the code looks like (the class names, methods parameters), and also the runtime information (the parameters, like usernames, and application specific information.) Sometimes there is a clear division between the two, sometimes they are blended together. Either way, we can assert that to be a DiC, a container must do at least one of:

  • Auto-instantiation: the ability to act as a factory, by creating objects. This means that the container is responsible for either calling or calling a method that will eventually call to create an object.
  • Auto-wiring: the ability to introduce one object as a dependency to another. This means if object A is required to successfully produce a valid object B, the container is capable of finding or producing object A before introducing it to object B. This could be through constructor injection or via setter injection.

In my book, failing to achieve either of those disqualifies a container from being a DiC.

What A Service Locator Is

A Service Locator, on the other hand, is better defined by a usage pattern, rather than the signature (pattern) of any particular class. This means that any object, upon being injected into a consuming object and then asked for an object, can be a service locator if it is capable of producing the requested object by a particular name or type. Effectively, a service locator can be a special container, or not; it can be a registry, or not; or it can be a factory, but perhaps not.

The only stipulation is that you’ve provided an object (the service locator) as a dependency to another object so that that the consuming object can then use the provided object (the service locator) to locate any dependencies. Instead of injecting n dependencies, you inject just one: the service locator. In code, it looks like this:

The constructor for this ThingThatHasDependencies, might look like this:

Now, you can see that any instance of ThingThatHasDependencies is now Container aware, which, depending on the context of the ThingThatHasDependencies, might be a good (or a bad) thing.

When to Use Which, In PHP

The decision of whether or not to use a Service Locator approach or use a DiC should be asked and answered not for your code base as a whole, but for each layer or component. The Service Locator pattern should be applied only in places where the it makes the most sense. There are a few simple questions that help you decide whether applying the Service Locator pattern to a particular component or layer within your application makes the most sense.

To reiterate, this is not a pattern that should be applied carte-blanche to all code. Not all code may benefit from using a Service Locator. To help you decide, start by dissecting the structure of your code into layers and components. For example, the layers in an MVC application are Models, Controllers and Views (in that order). The components can be any supporting library code that can stand on its own.

Once you’ve done this, the next question to ask of each is which part could benefit from the usage of a Service Locator vs. having explicitly defined class dependencies. To do this, there are a few considerations that help you decide:

  • What does an instance of a particular class best represent? Is it an object with a well defined API, or does the class better serve as a collection of somewhat related methods? (Loose API’s benefit from consuming a Service Locator.)
  • How many actual required dependencies are there? (Obj.’s with many dependencies benefit from a Service Locator)
  • Is it cumbersome to wire in a large set of dependencies? (Similar to above, put another way.)
  • How many “optional dependencies” are there? (Obj.’s with variable dependency sets benefit from a Service Locator)
  • How many required dependencies shared throughout all workflows of a given object? (Obj.’s with many dependencies benefit from a Service Locator.)
  • Is it anticipated that the number of dependencies might grow over time, or is it fixed? (Adding dependencies requires informing the consumer about how to wire new dependencies.)
  • How important is it that a particular class announce its dependencies in various method signatures? (More dependencies means more API introspection to determine how to wire an object.)
  • Is this piece of code at the center of many cross-cutting concerns? (Cross-cutting concern objects generally have lots of completely unrelated dependencies. As such, their API, like in point number 1, means little in terms of defining the objects actual role.)

In summary, if you find that if:

  • a layer of code or component has many dependencies that are not shared by all workflows of that particular consuming object;
  • that the number of these dependencies is large and capable of growing over time;
  • that there is not much importance on objects announcing their dependencies (via constructor injection, setter or interface injection);

then Service Location is by far a better solution than practicing dependency injection.

An Example/Exercise In Where I Make The Distinction

To better understand the considerations I’ve laid out above, I’ll demonstrate their application in an application’s architecture.

When dealing with an MVC application architecture, I prefer to practice service location for controllers, and dependency injection for models. So, how do I go about justifying that decision?

  1. Concerning controllers: The fact that a controller is an object and an action a method is inconsequential.

    Most actions could as easily do their job as a closure (like many Sintra style frameworks), or a function. While you get some benefits by being able to tuck away helper functionality in protected methods, the greatest driving factor for controllers as objects is simply that they can group contextually similar actions together into a class. Moreover, the public API of a controller is generally only ever interacted with via a dispatch framework. Rarely do developers instantiate and call controller actions without first going through some dispatcher object.

  2. Concerning controllers: Controller dependency sets are variable per controller.

    For any given action within a controller, it’s dependencies will vary. Given that you’ve grouped your User centric actions together, it goes without saying that while most might share some dependencies, it’s rare that every action will share all those dependencies. For example, a UserController::editAction might have a dependency on a UserForm object, but UserController::showAction might not:

  3. Concerning controllers: Injecting specific dependencies is generally outside the scope of a dispatcher.

    The job of a dispatcher is not trivial. Depending on the system, it could be parsing a HTTP route, and dissecting some information to determine what code to instantiate/invoke. Once it’s figured out what needs to be invoked, the last thing you want is your dispatcher is to have enough contextual understanding to make decisions on what specific services need to be injected into this particular controller. The following is a common dispatch workflow is non-complex frameworks:

  4. Concerning controllers: Controllers are very much tied to the stack that dispatches them.

    Controllers generally already have a dependency on the framework they are being dispatched from. Along the same lines of thinking, them understanding the type that $serviceLocator->get(‘db’); does not make them any less reusable since they are already dependent on a particular framework to dispatch them. (Unless you’re not using a framework, then this point is moot.)

  5. Concerning models: It is highly anticipated that models can be re-used outside of this particular application.

    That said, I find it important to be able to isolate dependencies and practice good DI in models. After all, they are most closely related to the business problem at hand, and since they are environment agnostic, they are a prime candidate for unit testing (whereas controllers are a better candidate for some form of UI or behavior testing). In addition, I find it important to see all dependencies up front when constructing models. Too many dependencies generally means it’s time to go back to the drawing board.

  6. Concerning models: The API of a model is something a developer (not a framework) interacts with.

    That being the case, the API needs to be able to effectively communicate any dependencies to the developer, without them having to consult large amounts of documentation.

Brief Parting Words

In short, contrary to a popular belief, Service Location is not always an anti-pattern. Patterns are not prescriptions, and service location is no exception. In some cases, using service location is beneficial, in others it is not.