4

Let's use PHP here for examples and illustraion purposes but the question is language and framework agnostic.

Many experts say Service Locator (anti)pattern should be avoided and recommend using Dependency Injection instead. Then we can find 3 ways of injection:

  • constructor injection
  • setter injection
  • field (property) injection

Those injection ways work fine when dependency is relatively static and serves the whole object lifecycle. But sometimes we need completely different thing - passing contextual dependencies that are required and relevant only for specific method calls.

For example:

<?php

interface IController {

    public function process($request);

}

class MyController implements IController {

    public function __construct(private $dep1, private $dep2, private $dep3) {
        ...
    }

    public function process($request, $dep4, $dep5) {
       ...
    }

}

While MyController instance along with its dependencies $dep1, $dep2, $dep3 exists for a long time and can process multiple requests, each process call requires also short living dependencies $dep4, $dep5.

The problem here is those dependencies are in fact an implementation detail and given IController interface may (should) just disallow process method to have something but $request argument.

With constructor injection that's quite fine because the constructor is not a part of any interface but just an implementation detail. This doesn't work for methods.

Even if we circumvent that we still are ought to pass those dependencies by hand, then we have no decoupling, no autowiring, etc.

With service locator though it's relatively easy since we can control context scopes transparently.

Is it fundamental Dependency Injection limitation and should we still use locators (either injected in constructor or passed to methods in form of context/"toolbox" objects)?

2
  • > While MyController instance along with its dependencies $dep1, $dep2, $dep3 exists for a long time and can process multiple requests This isn't true for a standard PHP setup if those are HTTP requests - the standard PHP engine shares nothing between separate requests, so each request has its own instance of the controller.
    – bdsl
    Commented Nov 13, 2023 at 16:54
  • public function process($request, $dep4, $dep5) - there's nothing wrong with this, this is no different than passing a callback (which is sometimes a closure, which is like passing an object - see, we've come full circle). You said "we still are ought to pass those dependencies by hand" - this is not neccesarily a problem. "then we have no decoupling" - what makes you say that? "no autowiring" - autowiring does not magically equal decoupling. It's the interface that the process method expects $dep4 and $dep5 to have that provides decoupling. That interface is not an implementation detail. Commented Nov 13, 2023 at 19:07

5 Answers 5

3

This is a framework problem disguised as a dependency problem.

Most web frameworks define a rigid set of interfaces and workflows for processing HTTP requests. This doesn't capture every way a dependency can be initialized, used, and destroyed. This question cannot be framework and language agnostic unless you can change the $request parameter as well, or change the code that initializes and calls your controller.

You have limited options to solve this problem within the confines of a specific web framework:

  1. Bite the bullet and use a service locator (my least preferred option).
  2. Pass these dependencies as constructor parameters to the controller, making the requisite changes necessary to do this (if possible).
  3. Create one or more factory objects whose purpose is to create these dependencies, and pass the factory objects as constructor parameters to the controller.

The difference between options 1 and 3 is a matter of scope and knowledge. A service locator brings with it the knowledge of all dependencies, most of which your controller does not need. This is a burden at this level of your application. Option 3 reduces the number of dependencies down to the factory, and the interface of the object that the factory returns.

My recommendation is to go with #3. Factories introduce a level of abstraction and obscurity, but give you the ability to create dependencies based on use case specific data without the added scope that a service locator provides.

Framework-specific issues aside, some web frameworks give you a way to inject controller method parameters from the DI container. The ASP.NET web framework allows you to annotate controller method parameters with [FromServices], which instructs the framework to get that object from the Dependency Injection container rather than the HTTP request. This should be the answer if your framework allows for this, but many do not.

Related: ASP.NET Core - Is using [FromServices] attribute bad practice?.

2

So you want to pass in a dependency that is not tied to the lifetime of the method-defining class instance. This dependency can be considered the "context" which the method should run in.

This question explores patterns for passing such a context down into a hierarchy of method calls.

One option to consider is along what Philip Kendall suggests in his answer: In OOP, any object fixes a context in which functions are run. So maybe you just need to define your method on your request object.

Another option is something you already identified: Use a Service or "ContextLocator" (those can be injected as regular dependencies) to retrieve your current request. In fact, the highest level could store this request for calls somewhere in the hierarchy, you just need a way to identify which request you are currently working in (one option is to have key it by thread).

You could also use setter injection to pass the request as a "temporary dependency", also this bears the danger that this might be forgotten or the dependency is stale.

Some languages even have constructs to pass in context objects, or have scopes to run stuff in, such as Kotlin's context receivers.

2

Those injection ways work fine when dependency is relatively static and serves the whole object lifecycle.

That’s not the fault of dependency injection. That’s not even the fault of a DI framework. That’s a mental model, where every class always has exactly one object, falling apart.

The-class-is-the-object model is a popular one but it often falls apart. When it does whatever tech was involved with it gets the blame.

Switching to service locater only helps here if whatever tech you were using makes it too difficult to see how to switch to a second object of the class. It works. It leaves you dependent on the service locator. But it works.

Pure DI doesn’t make you dependent on a locator. It will let you make as many objects as you’d care to construct. Which means even if the constructor sets state only once you can pass any of the objects you create with it around any time you want.

Most DI container frameworks let you do that as well. But they don’t make how you do it as obvious. That’s partly because they are trying to support the one-class-one-object mental model. But it’s really because they are trying to support convention-over-configuration. This strives to avoid forcing you to explicitly make decisions when they are boring typical ones.

The problem with that is when it comes time to make a non-typical decision you don’t have many good examples to work from. So much so it becomes easy to assume the DI framework is incapable of doing anything non-typical.

Well it can. It just takes a bit of work.

So no, you don’t have to use a service locator for this. All the forms of DI can handle it. If you can handle them.

1

My experience here is that this (anti-)pattern means you have failed to properly encapsulate your $request object; whatever changing behaviour you are trying to obtain by injecting $dep4 and $dep5 should instead be a method on $request, which can then own $dep4 and $dep5 itself. It may in fact be that your $request object is an anaemic domain model.

(For what it's worth, I'm much less against anaemic domain models than Fowler. But you need to recognise when you're using them and understand you need a different set of techniques to deal with them, not try and force them into patterns designed for use with rich domain objects).

3
  • Thanks for your answer. In common case the $request object structure, behavior and interface is not under our control. It could come from outside etc. It's supposed in the example we absolutely require dependencies for our specific implementation of process method. Neither caller not request emitter should not worry about this. Even if I encapsulate dependencies inside of $request it will be sorta service locator anyway.. Commented Nov 13, 2023 at 10:29
  • The behaviour of every object is under your control - you can encapsulate it in your own object if you need to. In any case, you said literally "the question is language and framework agnostic" so it's not about your specific $request object. Commented Nov 13, 2023 at 10:38
  • Unless what $dep4 and $dep5 are doing doesn't naturally belong inside the $request object. This is no different than passing a callback, and it gives you the same sort of flexibility (e.g. "for this call, invoke these objects when you're done, for this other call, invoke these other objects when you're done"). You can compose an object graph as needed, and then fire and forget. There are cases where this is actually a good design; this doesn't necessarily indicate an anemic model. The problem is that it's hard to judge if that's the case here with no context and only generic names. Commented Nov 13, 2023 at 19:15
1

With service locator though it's relatively easy since we can control context scopes transparently.

Service locators are the opposite of transparent. They remove visibility on exactly which dependencies are being used for anywhere outside the class using them.
By comparison, explicitly listing the dependencies means that outside consumers will be explicitly told what dependencies are needed (because they need to explicitly pass them as constructor parameters.

I get what you're going for. You want to keep the service locator around so that during the method call you can call for additional dependencies that weren't yet locked in during the constructor. That goal is good, but you're using the wrong tool for it.

What you need here is the factory pattern.

Factories can serve a few purposes. Some factories are good at deciding which subtype (out of many) to return to you without the consumer needing to know the chosen subtype. Some factories help you with constructing very difficult to construct objects.

But even when those cases don't apply, factories can still allow you to defer instantiation until the factory's method is called. Sound familiar?

What this means is that you don't inject the dependency, you inject the dependency's factory. During the method call, you then call the factory to create the "real" dependency. I'm no PHP expert but it would look something along the lines of:

<?php

interface IController {

    public function process($request);

}

class MyController implements IController {

    public function __construct(
        private $dep1, private $dep2, private $dep3, 
        private $dep4factory, private $dep5factory
    ) {
        ...
    }

    public function process($request) {
       $dep4 = $dep4factory.create();
       $dep5 = $dep5factory.create();

       // Your usual logic that makes use of $dep4 and $dep5
    }

}

Note that a factory can be very simple. Sorry if this is bad PHP, but it can be as simple as:

class FooFactory {
   public function create() {
       return new Foo();
    }
}

Note also that in this case, you need to register FooFactory in your DI container, not Foo.

Additionally, I've worked with DI container which even allow for "automatic" factories out of the box. My experience is in C#.NET, but it's possible that there's a PHP equivalent for it.

This is a C# example, but NInject (DI container library) allows you to register Foo in your DI container, but request a Func<Foo> (i.e. method that returns a Foo) as your injected dependency type, and the container will figure out that you need a factory around the Foo dependency and will create a factory method on the fly. I don't know if PHP has libraries/provisions for similar behavior, but it's not that hard to create simple factories and just solve it that way.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.