Dependency Inversion

Available

9.1 Dependency Inversion

Remember, we once said that in a server application you can’t just create streams through new Thread().start()? Only the container should create threads. We will now develop this idea even further.

All objects should also be created only by the container . Of course, we are not talking about all objects, but rather about the so-called business objects. They are also often referred to as bins. The legs of this approach grow from the fifth principle of SOLID, which requires getting rid of classes and moving to interfaces:

  • Top-level modules should not depend on lower-level modules. Both those, and others should depend on abstractions.
  • Abstractions should not depend on details. The implementation must depend on the abstraction.

Modules should not contain references to specific implementations, and all dependencies and interactions between them should be built solely on the basis of abstractions (that is, interfaces). The very essence of this rule can be written in one phrase: all dependencies must be in the form of interfaces .

Despite its fundamental nature and apparent simplicity, this rule is violated most often. Namely, every time when we use the new operator in the code of the program/module and create a new object of a specific type, thus, instead of depending on the interface, the dependence on the implementation is formed.

It is clear that this cannot be avoided and objects must be created somewhere. But, at the very least, you need to minimize the number of places where this is done and in which classes are explicitly specified, as well as localize and isolate such places so that they are not scattered throughout the program code.

A very good solution is the crazy idea of ​​concentrating the creation of new objects within specialized objects and modules - factories, service locators, IoC containers.

In a sense, such a decision follows the Single Choice Principle, which says: "Whenever a software system must support many alternatives, their complete list should be known only to one module of the system" .

Therefore, if in the future it is necessary to add new options (or new implementations, as in the case of creating new objects we are considering), then it will be enough to update only the module that contains this information, and all other modules will remain unaffected and will be able to continue their work. as usual.

Example 1

new ArrayList Instead of writing something like , it would make sense List.new()for the JDK to provide you with the correct implementation of a leaf: ArrayList, LinkedList, or even ConcurrentList.

For example, the compiler sees that there are calls to the object from different threads and puts a thread-safe implementation there. Or too many inserts in the middle of the sheet, then the implementation will be based on LinkedList.

Example 2

This has already happened with sorts, for example. When was the last time you wrote a sorting algorithm to sort a collection? Instead, now everyone uses the method Collections.sort(), and the elements of the collection must support the Comparable interface (comparable).

If sort()you pass a collection of less than 10 elements to the method, it is quite possible to sort it with a bubble sort (Bubble sort), and not Quicksort.

Example 3

The compiler is already watching how you concatenate strings and will replace your code with StringBuilder.append().

9.2 Dependency inversion in practice

Now the most interesting: let's think about how we can combine theory and practice. How can modules correctly create and receive their “dependencies” and not violate Dependency Inversion?

To do this, when designing a module, you must decide for yourself:

  • what the module does, what function it performs;
  • then the module needs from its environment, that is, what objects / modules it will have to deal with;
  • And how will he get it?

To comply with the principles of Dependency Inversion, you definitely need to decide which external objects your module uses and how it will get references to them.

And here are the following options:

  • the module itself creates objects;
  • the module takes objects from the container;
  • the module has no idea where the objects come from.

The problem is that to create an object, you need to call a constructor of a specific type, and as a result, the module will depend not on the interface, but on the specific implementation. But if we do not want objects to be created explicitly in the module code, then we can use the Factory Method pattern .

"The bottom line is that instead of directly instantiating an object via new, we provide the client class with some interface to create objects. Since such an interface can always be overridden with the right design, we get some flexibility when using low-level modules in high-level modules" .

In cases where it is necessary to create groups or families of related objects, an Abstract factory is used instead of a Factory Method .

9.3 Using the Service Locator

The module takes the necessary objects from the one who already has them. It is assumed that the system has some repository of objects, in which modules can “put” their objects and “take” objects from the repository.

This approach is implemented by the Service Locator pattern , the main idea of ​​which is that the program has an object that knows how to get all the dependencies (services) that may be required.

The main difference from factories is that Service Locator does not create objects, but actually already contains instantiated objects (or knows where / how to get them, and if it creates, then only once at the first call). The factory at each call creates a new object that you get full ownership of and you can do whatever you want with it.

Important ! The service locator produces references to the same already existing objects . Therefore, you need to be very careful with the objects issued by the Service Locator, since someone else can use them at the same time as you.

Objects in the Service Locator can be added directly through the configuration file, and indeed in any way convenient for the programmer. The Service Locator itself can be a static class with a set of static methods, a singleton, or an interface, and can be passed to the required classes via a constructor or method.

The Service Locator is sometimes called an anti-pattern and is discouraged (because it creates implicit connections and only gives the appearance of good design). You can read more from Mark Seaman:

9.4 Dependency Injection

The module doesn't care about "mining" dependencies at all. It only determines what it needs to work, and all the necessary dependencies are supplied (introduced) from the outside by someone else.

This is what is called - Dependency Injection. Typically, the required dependencies are passed either as constructor parameters (Constructor Injection) or through class methods (Setter injection).

This approach inverts the process of creating dependencies - instead of the module itself, the creation of dependencies is controlled by someone from the outside. The module from the active emitter of objects becomes passive - it is not he who creates, but others create for him.

This change in direction is called the Inversion of Control , or the Hollywood Principle - "Don't call us, we'll call you."

This is the most flexible solution, giving the modules the greatest autonomy . We can say that only it fully implements the “Single Responsibility Principle” - the module should be completely focused on doing its job well and not worrying about anything else.

Providing the module with everything necessary for work is a separate task, which should be handled by the appropriate “specialist” (usually a certain container, an IoC container, is responsible for managing dependencies and their implementation).

In fact, everything here is like in life: in a well-organized company, programmers program, and the desks, computers and everything they need for work are bought and provided by the office manager. Or, if you use the metaphor of the program as a constructor, the module should not think about wires, someone else is involved in assembling the constructor, and not the parts themselves.

It would not be an exaggeration to say that the use of interfaces to describe dependencies between modules (Dependency Inversion) + the correct creation and injection of these dependencies (primarily Dependency Injection) are key techniques for decoupling .

They serve as the foundation on which the loose coupling of the code, its flexibility, resistance to changes, reuse, and without which all other techniques make little sense. This is the foundation of loose coupling and good architecture.

The principle of Inversion of Control (together with Dependency Injection and Service Locator) is discussed in detail by Martin Fowler. There are translations of both of his articles: "Inversion of Control Containers and the Dependency Injection pattern" and "Inversion of Control" .

Comments
  • Popular
  • New
  • Old
You must be signed in to leave a comment
This page doesn't have any comments yet