Reducing call sites with dependency injection and context passing

Posted on by Matthias Noback

This article continues where Unary call sites and intention-revealing interfaces ended.

While reading David West's excellent book "Object Thinking", I stumbled across an interesting quote from David Parnas on the programming method that most of us use by default:

The easiest way to describe the programming method used in most projects today was given to me by a teacher who was explaining how he teaches programming. "Think like a computer," he said. He instructed his students to begin by thinking about what the computer had to do first and to write that down. They would then think about what the computer had to do next and continue in that way until they had described the last thing the computer would do... [...]

It may seem like a very logical thing to do. And it's what I've seen myself and many other programmers do: "How do we implement this feature?" "Well, first we need this piece of data. Then we need to have a little algorithm manipulating it. Then we need to save it somewhere. And then, and then, etc." The code that is the result of this approach is sequential, imperative in nature.

Parnas continues:

Any attempt to design these programs by thinking things through in the order that the computer will execute them leads to confusion and results in systems that nobody can understand completely.

Parnas, David Lorge. "Software Aspects of Strategic Defense Systems." American Scientist 73 (1985). pp. 432–440.

The book describes ways to counter this method using a different style of thinking called "object thinking". When I know more about it, I'll share it with you. For now, I just wanted to note that this "computer thinking" we do, leads to many issues that make the overall design of our application worse. I described one situation in a previous article about read models, where I realized that we often try to answer many different questions by querying one and the same model (which is also the write model). By splitting write from read, we end up with a more flexible design.

The same goes for introducing interfaces, to achieve dependency inversion. We do a little extra work, but we thereby allow ourselves to get rid of a bit of "computer think".

Singletons, service locators, registries...

In the past, we've invented some useful utilities to help us with our computer thinking. "We need to send an email for this feature." "Hmm, how do we get the mailer?" "I know! We get it from the service locator." So there we go:

$mailer = $this->container->get('mailer');

"What else do we need?" "The current Request object, so we can find out if the client has added a particular header." "Okay, let's add that the request to the service locator as well." "No, wait, the request is part of the context." "No problem!"

$request = sfContext::getInstance()->getRequest();

"Wait, the context hasn't been created yet? Let's tell the computer to do it for us."

$request = sfContext::createInstance()->getRequest();

"Wait, the context has sometimes been created and now creating another instance leads to subtle bugs and decreased performance?"

if (sfContext::hasInstance()) {
    $request = sfContext::getInstance()->getRequest();
} else {
    $request = sfContext::createInstance()->getRequest();
}

Okay, this is where we should stop the silliness. This story is over the top. But we do this kind of thing every day. We write code that will break. Not now, but once it's used in slightly different ways (in a not so far future).

We're lucky, since we can prevent this situation from happening. All the answers are there. They are old, and famous answers.

Dependency injection

One of the answers is: dependency injection. To prevent us from worrying about "how to get something", we have to assume we'll get it. In other words, when constructing an object, dependencies (like the mailer from the previous example), should be injected. No need to fetch it, when the code gets executed, it will be there:

final class SomeService
{
    public function __construct(Mailer $mailer)
    {
        $this->mailer = $mailer;
    }

    public function doSomething()
    {
        $request = ...
    }
}

Of course we need a little setup work outside SomeService to make sure the mailer gets injected. But this is completely out of sight.

Context passing

Another answer is: context passing (not sure if this is an official term or anything, just posing it here). Consider all the objects in your application to be eternal. For instance, that mailer service we used; it could run forever. We could let it send any number of emails by calling its send() method with different arguments. However, the current request is a completely different object. It's impossible to model it as some eternal service, since it's temporary in nature. It comes and goes. In fact, asking for the current request will give you different answers all the time.

If you need an object like the current request (or the session, the user ID, the tenant ID, etc.), don't inject it in the constructor of a service - pass it along as an argument when calling the method:

final class SomeService
{
    public function __construct(Mailer $mailer)
    {
        $this->mailer = $mailer;
    }

    public function doSomething(Request $request)
    {
        ...
    }
}

By the way, context passing doesn't necessarily mean that you should be passing a Context object. In several projects I've worked on, we introduced a Context object at some point, to hold things like the ID of the user who owns the current session, a tenant or customer ID, and miscellaneous things like the client's IP address, or the request time. This may seem very convenient, but such a Context object itself becomes very unfocused. We are tempted to add more and more to it. Maybe user settings, maybe entire entities (just so we don't need to make a call to the repository anymore). In conclusion, passing around a Context object is often the first step in separating dependencies from context, but there's always another step after that: passing only the values from the context that a given method needs.

Dependency injection & Context passing: effective means for reducing call sites

In projects that don't apply dependency injection and context passing (everywhere), you will find that there are issues with the number of call sites for certain methods. In particular: ServiceLocator::get() or things like Context::getRequest(). If we apply "computer think", we use these static methods to fetch all the things we need, instead of receiving them as constructor or method arguments. If we use "object thinking", we get rid of these static calls, and so we drastically reduce the number of call sites for them. This in turn allows us to prepare the project for the future. Because today's ServiceLocator is yesterday's façade, last week's Zend_Registry, and last month's sfContext. Instead of using whatever convenient utility your favorite framework uses for fetching things, just make sure you always inject your dependencies and pass along your context. If you always do this, you can easily migrate your project once the framework you use isn't maintained anymore (or when you just want to try out something better).

PHP legacy legacy code code quality call sites object design
Comments
This website uses MailComments: you can send your comments to this post by email. Read more about MailComments, including suggestions for writing your comments (in HTML or Markdown).
roomcays

I agree. I'm always in doubt when using things like service locators, registers, etc. The thing that makes me confused is that I fell the DIC-philosophy "promoted" by every PHP framework I think of, even my favourite skinny Slim Framework. This makes the temptation to not think about direct dependency injection or context passing even stronger...

Nico Haase

"Posted on Feb 2nd 2018" -- interesting piece from the future :)

gskema

We come into this at work every day. It's very annoying, also bad code. My rule of thumb is: the argument (e.g. Request) should be determined and passed down as early as possible (e.g. in HTTP controller). I often see Request injected separately into Service1 and Service2. Then Service1 calls Service2! There's no guarantee that these two services are even working with the same object! By passing it down as an argument it's much clearer what's going, what are the argument, there are no hidden argument "behind the scenes". The code becomes "deterministic" and easier to test.

Matthias Noback

Horrible story you got there about Service1 and 2 :) Totally agree. Also, I like to create values or value object based on request data to prevent myself from passing around the Request object itself.