What is the main difference between Dependency Injection and Service Locator?

Ha, what a graceful disguise of the Service Locator under DI. It might even seem like a DI! :-)

This is the first comment on my previous post, Dependency Injection, JavaScript, and ES6 Modules . Thanks to colleague symbix 'u for this comment, tk. it was he who became the cause of immersion in the subtleties of the difference of one from another. Under cat, my answer to the question is in the title.







image







(KDPV does not make much sense and is intended primarily for the visual identification of this publication, among others)







To begin with, let's deal with the definitions ( some of the code examples I will cite in PHP, some in JS, because these two languages ​​are currently in my active luggage, and some in any other, because I stole these examples from the Internet ).







Dependency injection



DI in a nutshell - instead of the require / import module, you inject the dependency through the constructor parameter (or property setter). That is, behind this big word is the simple “pass class dependencies through constructor parameters”.

This comment is risedphantom's colleague quite accurately conveys the essence of the phenomenon. To facilitate the developer’s understanding of the code, all dependencies are described explicitly - in the form of constructor parameters (usually, but not always):







class MainClass { public function __construct($dep1, $dep2, $dep3) {} }
      
      





All. DI - he is about it. The dependencies we need are provided by someone there. And where this “someone out there” takes them - DI does not care.







When analyzing the code, it is important to understand what exactly is “internal” for it (and what we can safely change), and what comes / goes beyond the responsibility of this code. This is what excites DI. Why are dependencies mostly passed through the constructor, and not in setters? Because it’s more convenient for the developer - he immediately sees all the dependencies of the analyzed code in one place. We are used to thinking that DI is something at the class level, but function / method parameters are also DI:







 function f(dep1, dep2, dep3) {}
      
      





A constructor is just such a special method among all others.







Service locator



Service Locator is a well-known anti-pattern. What does he do? Provides access to one object to other objects. Here is a typical interface for such a locator:







 interface ServiceLocator { public function addInstance(string $class, Service $service); public function addClass(string $class, array $params); public function has(string $interface): bool; public function get(string $class): Service; }
      
      





A locator is a container in which you can put ready-made objects (or set the rules for their creation) and then access these objects. To a locator, by and large, it does not matter how the objects appear in it - they are explicitly created from the outside and placed in the container or created by the container itself according to the given rules.







The service locator is responsible for storing objects and providing access to them. All.







Dependency Injection Container



What is a DI container? According to the freelance retelling of the contents of " Dependency Injection Principles, Practices, and Patterns ": "a software library that that provides DI functionality and allows automating many of the tasks involved in Object Composition, Interception, and Lifetime Management. DI Containers are also known as Inversion of Control (IoC) Containers. (§3.2.2) "







That is, the DI container is primarily responsible for creating objects, but only in the second - for their storage. A DI container may not even store a single created object if the application itself does not need objects that are common to the entire application (such as a configuration or logger).







Here, for example, is the Symfony DI container interface:







 interface ContainerInterface extends PsrContainerInterface { public function set(string $id, ?object $service); public function get($id, int $invalidBehavior = self::EXCEPTION_ON_INVALID_REFERENCE); public function has($id); public function initialized(string $id); public function getParameter(string $name); public function hasParameter(string $name); public function setParameter(string $name, $value); }
      
      





If necessary, you can very easily convince yourself that the interface of the DI container is very similar to the interface of the Locator Services - the same get



, has



and set



/ add



.







Why is the Service Locator bad?



But nothing. Not the template itself is bad, but the way it is sometimes used. In particular, there is an opinion that " Service Locator violates encapsulation in statically typed languages, because this pattern does not clearly express preconditions ." And an example violation:







 public class OrderProcessor : IOrderProcessor { public void Process(Order order) { var validator = Locator.Resolve<IOrderValidator>(); if (validator.Validate(order)) { var shipper = Locator.Resolve<IOrderShipper>(); shipper.Ship(order); } } }
      
      





Like, that's so bad, but good - like this:







 public class OrderProcessor : IOrderProcessor { public OrderProcessor(IOrderValidator validator, IOrderShipper shipper) public void Process(Order order) }
      
      





Hey, we just IOrderValidator



out the brackets of the moment of creating the objects themselves with the IOrderValidator



and IOrderShipper



! It is possible that in this application somewhere else there is something like this code:







 var validator = Locator.Resolve<IOrderValidator>(); var shipper = Locator.Resolve<IOrderShipper>(); var processor = new OrderProcessor(validator, shipper); processor.Process(order);
      
      





Composition root



Speaking of Dependency Injection, we cannot help but arrive at a concept such as Composition Root (hereinafter I will call it the "Assembly Point"). This is the same “ someone ” to whom the responsibilities for creating dependencies and their implementation are delegated.







At the Assembly Point, all dependencies are explicitly defined, the corresponding objects are created and implemented where they are waiting. Here the differences between the Services Locator and the DI container are minimal. Both that and another allow to create new objects, to store the created objects, to extract stored objects. I would even undertake to assert that in this place there are no fundamental differences between them.







The main difference



But where are the differences between the DI container and the container of the Services Locator most obvious? In any other place, and especially with static access to the container:







 $obj = Container::getInstance()->get($objId);
      
      





Here it is. In this form, anywhere (unless it is an Assembly Point, but the use of static access is very doubtful there), any container becomes an anti-pattern called Service Locator.







A VolCh colleague briefly and succinctly answered my question :







And how do you think true DI differs from a Service Locator disguised as a DI?

So:







Container access

In fact, this entire publication is just a more detailed deployment of his answer here.







Legal use of the container



Thus, whether a Container is a DI container or a container for the Services Locator depends very much on where and how we use it. As I said above, at the Assembly Point, the difference between the types of containers disappears. But what do we pass if the container itself is a dependency for any class?







 class Foo { public function __construct(Container $container){} }
      
      





Again, it all depends on how we use the container. Here are two examples, one of which exactly matches the container of the Services Locator and is an anti-pattern, while the other has the right to life under certain conditions.







 class Foo { private $dep; public function __construct(Container $container) { $this->dep = $container->get('depId'); } }
      
      





 class Foo { private $container; public function __construct(Container $container) { $this->container = $container; } public function initModules($modules) { foreach ($modules as $initClassId) { $init= $this->container->get($initClassId); $init->exec(); } } }
      
      





Summary



In fact, everything is somewhat more complicated, the DI container should have broader functionality than the Services Locator, but the essence of this publication is that even the most sophisticated DI container can be easily used as a Services Locator, but to use the Services Locator as a DI container - you need to try.







PS

I can’t help but provide links to Sergei Teplyakov’s Programming stuff blog - these questions are very well covered there.








All Articles