Service locator vs Dependency injection

During designing application, we are making new classes. Classes have dependencies. In programming we distinguish two patterns for managing class’s dependencies:

  • Dependency injection
  • Service locator

Dependency injection

In the following code, class Service perfectly shows us its dependencies. We know that we have to inject Logger to the constructor to create an instance of this class.

<?php

use Psr\Log\LoggerInterface;

class Service
{
    private $logger;

    public function __construct(LoggerInterface $em)
    {
        $this->logger = $logger;
    }

    public function doSomething()
    {
        $this->logger->info('...');
    }
}

Service locator

A simple example of using service locator is something like that:

<?php

use Psr\Container\ContainerInterface;

class Service
{
    private $container;

    public function __construct(ContainerInterface $container)
    {
        $this->container = $container;
    }
    
    public function doSomething()
    {
        $this->container->get('logger')->info('...');
    }
}

Tell me now, what are dependencies of Service class? Yeah, of course, you don’t know. We can’t define it precisely. We only know, that class requires the Container, but from the container, it can get any dependency.

Service locator as anti-pattern

For reason, that service locator hides class’s dependencies is considered as an anti-pattern. Hidden dependencies are making testing code harder. We can’t in an easy way mock these dependencies.

Magic of Laravel

In Laravel, we have something called Facades. Many people are hating laravel for this. Facades give us the same problem as Service locator. They hide dependencies of class. If you look at the constructor of a class you don’t know what are dependencies. Also, you can’t mock them. We can say that facades are even worse, because we can access them everywhere. Facade in a template, no problem.

How it works?

Log::info('...');

We are seeing :: operator, so we can think that in class Log should be static method info(), nothing could be further from the truth. In fact every facade inherits from the base class magic method like following:

public static function __callStatic($method, $args)
{
    $instance = static::getFacadeRoot();
    if (! $instance) {
        throw new RuntimeException('A facade root has not been set.');
    }
    return $instance->$method(...$args);
}

It follows that it isn’t static call, but instance method call.

Tying with framework

Let’s compare two examples of code. First when we have code which is using dependency injection like following.

<?php

final class Service implements ServiceInterface
{
    private $logger;

    public function __construct(LoggerInterface $logger)
    {
        $this->logger = $logger;
    }

    public function doSomething(): void
    {
        $this->logger->info('...');
    }
}

Second with laravel’s facades.

<?php

final class Service implements ServiceInterface
{
    public function doSomething(): void
    {
        Log::info('...');
    }
}

In the first example, we can in easy way use code with another framework. Our class doesn’t know what framework we’re using, so it isn’t a problem. While in the second example, if we want to use it with another framework, we have to do some refactoring like removing laravel’s facades. So using laravel’s facades we are tying our code with laravel. It isn’t good practice.

Summary

If you want to write code easy testable and maintainable you should avoid using service locator and facades. Dependencies of the class have to be exposed, not hidden. Just use Dependency injection.

Share: