Dependency injection smells

Posted on by Matthias Noback

The Symfony2 DependencyInjection Component has made my life and work as a developer a lot easier. Choosing the right way to use it however can be a bit difficult sometimes. Knowing what a/the service container can do, helps a lot, and also thinking about how you would do it with just PHP can put you back on track. To be able to recognize some problems related to dependency injection in your own code, I will describe a few "dependency injection smells" below (a term derived from "code smells", used by Kent Beck, Martin Fowler and the likes).

Injecting too many dependencies

When you choose constructor or setter injection (I would not recommend any other way), you would recognize this problem by a large number of injected objects, i.e. services:

class UnfocusedClass
{
    public function __construct(
        EntityManager $entityManager,
        DocumentManager $documentManager,
        NewsFeedGenerator $generator,
        DelayedEmailSender $emailSender,
        RouterInterface $router,
        LoggerInterface $logger,
    ) {
        // put them all into private properties
    }
}

When you look at a class and its dependencies it should already be clear what the class can do, but also that it does not do too much. Looking at the list of dependencies above, this class clearly does too much and knows all too well about all these things. It has, in other words, no single responsibility (not even two responsibilities). Cut this thing into several other classes, and leave all the implementation details to them.

Injecting dependencies just for passing them through

This happens when a class receives a dependency just to use it later on when instantiating some other class (or otherwise to pass as an argument to a method of another object). For instance:

use Symfony\Component\Translation\Translator;

class MenuItem
{
    private $translator;

    public function __construct(Translator $translator)
    {
        $this->translator = $translator;
    }

    public function createView()
    {
        return new MenuItemView($this, $this->translator);
    }
}

There are several problems with the design of this class:

  1. The MenuItem is now tightly coupled to the MenuItemView class. This is because we used the new operator. It would not be so easy for other developers to modify the behavior of this class because he can't use his own view class.

  2. Injecting the translator does not look too bad at first sight, but now we know that it's only used in the createView() method: what if the MenuItemView class needs more dependencies in the future? Should we keep injecting them all into the constructor? This is bad - MenuItem itself does not need these dependencies, MenuItemView does!

The solution would be to create a factory for creating the view items. The factory should know about MenuItem and MenuViewItem, and MenuItem will know nothing about creating a view item anymore:

use Symfony\Component\Translation\Translator;

class MenuItemViewFactory
{
    private $translator;

    public function __construct(Translator $translator)
    {
        $this->translator = $translator;
    }

    public function create(MenuItem $menuItem)
    {
        return new MenuItemView($menuItem, $this->translator);
    }
}

class MenuItem
{
    // contains only the data for the menu item,
    // no translator, no createView() method
}

Injecting implementations instead of interfaces

You may have already noticed this injection problem when you looked at the MenuItemViewFactory class. Its constructor expects a Translator object. We should have used the TranslatorInterface instead, since Translator just implements that interface:

use Symfony\Component\Translation\TranslatorInterface;

class MenuItemViewFactory
{
    public function __construct(TranslatorInterface $translator)
    {
        // ...
    }
}

"Why?" would you say, "I'm never going to use anything else then the standard Translator bundled with the framework!" You could be right about that, but there are many other cases where you will change implementation later on. But that's not really the point: you only really need a dependency to implement the interface. In almost all cases it does not matter what implementation of a translator you get, as long as it can translate some string and replace some variables within it (exactly the expected behaviour of TranslatorInterface::trans()). The interface is a contract, and each implementation promises to provide what the interface defines. So you should trust the interface and use it when injecting dependencies.

Making all services public (or forgetting to make them private)

When you dump your service container (using app/console container:debug), and you see many of your services that should not or do not need to be used stand-alone, you should mark them as private. By default, all services you define are publicly available. This means that every part of the application can retrieve them by calling (when $this->container is the service container):

$this->container->get('public_service);

Marking services as private (in fact, you should set the "public" attribute of the service to "false") will clean things up a lot. It will also make it clear to other developers what the main entry points of your bundle are.

Before making all your services private (except one or so...), remember that some lazy-load mechanisms require a service to be public. For instance, all form types registered using the form.type tag must be public, since they are retrieved from the service container only when needed, by using the get() method of the container.

Having an unreadable service definition file

Both Yaml and XML service definition files should be as readable as they can be. This means: add some extra lines between each service definition, group services and their dependencies with comments and put them in a readable order (and be consequent from them on).

Also, when some logical subdivisions can be made in your service definition file (be it services.xml or services.yml), it may also help maintainability to split it into multiple files. Of course, these new files should then also be loaded from your bundle extension:

namespace Matthias\SomeBundle\DependencyInjection;

use Symfony\Component\HttpKernel\DependencyInjection\Extension;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Loader\XmlFileLoader;
use Symfony\Component\Config\FileLocator;

class SomeExtension extends Extension
{
    public function load(array $config, ContainerBuilder $container)
    {
        $locator = new FileLocator(__DIR__.'/../Resources/config');
        $loader = new XmlFileLoader($container, $locator);
        $loader->load('services.xml');
        $loader->load('form.xml');
    }
}

This way, you could even make the file optional, by wrapping the $loader->load() command in an if-statement.

In conlusion

Two things: service definition files are also part of your project, which means they should be readable and maintainable. And: just because injecting dependencies is so easy these days, think well about where and why you inject them.

These were some of the more important "dependency injection smells". I'm happy to hear of any other "smells" you have encountered yourself.

PHP Symfony2 dependency injection service container
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).
Dad Bot 3000

I'm thinking that DI Container should not be used beyond the controller.

Dejan Spasic

Some good catches here.

But it’s more about issues with the Dependency Injection *Container* which are discussed here instead of the design pattern.

Here an example from the official documentation with an example of the limitation of the dependency injection :
http://symfony.com/fr/doc/c...

protected function getUploadRootDir() {
return __DIR__.'/../../../../web/'.$this->getUploadDir();
}

You can't move your file from its directory, because the path is hardcoded !

The right way is to use the Kernel getRootDir() method , but you can't access the Kernel directly into your entity.

For every Entity you create with a relation with the file system you will have to inject the kernel.
Upload / Pictures ...

(You can manage this by a Service, embedding the Entity, but anyone loading your bundle can instanciate an entity that will not be self-suffisant and can crash everything, because the code logic is now ... in the Service)

Juzer Ali

I am not a fan of constructor injection. It kind of takes a step back on the progress we have made on decoupling. Especially when testing a service, say your are testing a method that utilizes just one dependency, you still have to mock all the other dependencies to instantiate the class.

Garrett Granacher

If you're having to mock all kinds of crazy dependencies to unit test the class, then the class probably does too much -- violating Single Responsibility Principle. Not saying I'm perfect at this by any means! I still have to work at it...

Alexander Volochnev

Thank you, Matthias, it's a very useful article.

Tom B

Nice article! I wrote about the pitfalls (with examples) of the "Ask for dependencies just to pass them through" problem back in October: http://r.je/oop-courier-ant... but it's good to see some of the other DI related issues you've addressed!

Matthias Noback

Ha, that's great! It's nice to read about the same problems in your post and calling them anti-patterns and code smells. It's an important subject for OO developers. Thanks,

cordoval

can setter injection be abused?

cordoval

if so what is the cure? how to approach this, in my team i have seen the pattern and i am not sure if object or let it loose

Matthias Noback

In my experience, setter injection is only useful when you extend an existing service (i.e. extend a class and/or a service definition), and don't want to change the signature of the constructor, because other services depend on its arguments to be in a certain order/be of a certain number. In most cases, setter injection results in class attributes not set ;)

Lukas

A Symfony2 specific one:

Injecting the container to "get" the a service in the constructor so that one doesnt have to set the service scope to "request" because of a "request" scoped dependency. In this case either set your service to "request" scope as well .. or if you are absolutely certain that this cannot have problematic side-effects simply configure the DIC to ignore the error. That is at least a clear way to specify that you choose to ignore the issue rather than hiding it in the constructor.

gggeek

nice one. But it seems quite a common case: Twig extensions. They have to be set to global scope if they want f.e. to use the asset service. But then they will not work in CLI mode...