📜 ⬆️ ⬇️

Understanding SOLID: Inversion of Dependencies

Let's look at the definition of the principle of dependency inversion from Wikipedia:


The principle of dependency inversion (eng. Dependency inversion principle, DIP) is an important principle of object-oriented programming used to reduce connectivity in computer programs. Included in the top five principles of SOLID.

Formulation:

A. The modules of the upper levels should not depend on the modules of the lower levels. Both types of modules must depend on abstractions.
B. Abstractions should not depend on details. Details must depend on abstractions.

Most of the developers with whom I have communicated understand only the second part of the definition. They say "well, what's wrong with that, you need to tie classes not to a specific implementation but to the interface". And it seems to be true, but only to whom should the interface belong? And why is this principle so important at all? Let's figure it out.


Modules


module - a logically interrelated set of functional elements.

To avoid misunderstandings, we introduce some terminology. By module we will mean any functionally related part of the system. For example, we can place the framework as a separate independent module, and the logic of working with users into another.


A module is nothing more than an element of the system decomposition. A module may include other modules, forming something like a tree. Accordingly, you can select modules of different levels:


Dependency graph

Here, the arrows between the modules show who uses what. Accordingly, the same arrows will show us the directions of dependencies between our modules.


And now it's time to add "one more button". And we understand that the functionality of this button is implemented in the E module. We did not hesitate to add what we needed, and we had to change the interface of interaction with our module.


We already wanted to close the task, commit the code ... but we changed something ... let's go and see if we broke anyone. And it turns out that module B has broken because of our changes. Okay. Repaired. What if someone is using module B is also broken? Indeed! Module A also fell off. We repair ... We commit, we push. Well, if there are tests, then we will know about the problem quickly and we can fix it. But let's face it, few people write tests.


And also a bug from the tester came to your colleague, that the module C was broken. It turned out that he was carelessly tied up on your module E , and did not tell you about it. Moreover, this module consists of a heap of files, and everyone needs something from module E. And now he climbs on his part of the dependency graph (because it is easier for him to navigate in it than for you, but not your part of the system) and curses you.


Changes in the dependency graph

In the picture above, the orange circle indicates the module that we wanted to fix. And the red ones - which had to be corrected. And not the fact that each circle - one class. It can be whole components. And it’s good if we don’t have many modules and they don’t intersect much. And what if every circle would be connected with each? Well this is to fix everything on any sneeze. And as a result, the simple task "add a button" turns into a refactoring of a piece of the system. How to be?


Interfaces and late binding


Late binding means that an object is associated with a function call only at runtime, and not at compile time.

As you know, interfaces define a contract. And each object implementing this contract is obliged to comply with it. For example, we write user registration. And remember the requirement - the user's password must be reliably hashed in case of data leakage from the database. Suppose that at the moment we do not know how to do it correctly. And suppose that we have not yet chosen a framework or libraries to do the project. Madness, I know ... But let's imagine that we now have nothing but the logic of the application.


We remember the demand, but do not give up everything to us? Let's still finish with the user registration first, and then we will figure out how to do something. We must still consistently approach the work. So, instead of googling "how to properly hash a password" or figure out how to do this in our framework, let's make the PasswordEncoder interface. By doing this, we will create a "contract". Like, anyone who decides to implement this interface is obliged to provide secure and secure password hashing. The very same interface will be madly simple:


 interface PasswordEncoder { public function encode(string $password): string; } 

This is exactly what we need to work at a given time. We do not want to know how this will happen, we still do not know about salt and slow hashing. We can make a stub, which at the time of development will return what we have pushed. And then we will make a normal implementation. Similarly, we can do with sending an email that we have successfully registered a user. We can even put in parallel people who will implement these interfaces for us, so that things go faster. Beauty.


And the beauty is that we can dynamically replace the implementation. That is, just before calling the user registration, choose which password encoder we need to use. This is what is meant by late binding. The ability to "select" the implementation right before using it.


In languages ​​with a dynamic type system, such as in PHP, there is an even easier way to achieve late binding - do not use type hinting. From the word at all. True, having done this, we completely lose static (represented explicitly in the code) information about who uses what. And when we change something, it won't be easy for us to determine if the code is broken. This is how to turn off the light and look for paired socks in the mountain of 99 one left and one right.


Inversion of dependencies


So, we have already decided that the module E breaks everything. And your colleague wanted to protect against future changes in the "foreign" code. After all, it uses only one function from this module.


To do this, in his C module, he created an interface, and wrote a simple adapter that accepts a dependency from the required module and provides access only to the necessary method. Now, if you fix something, you can fix the "damage" in one place.


Moreover, this interface is located on the border of module C , when the adapter is on the border of module E. Like, when the developer of the module E comes into his head to fix his code, he will have to repair our adapter.


Well, we decided that soon we will rewrite this module altogether and we should also protect our dependent module. Since we use more of the E module, we don’t use the interface of your colleague. We need to realize our own. We also have to implement this interface as part of the E module, so that later, when we rewrite it, do not forget to correct the implementation. Let's see what happened with us:


Inversion dependency graph

It is very important that we have two interfaces, and not one . If we placed the interface in module E , we would not eliminate the dependencies between the modules. Moreover, different modules require different capabilities. Our task is to isolate exactly the part that we are going to use. This will greatly simplify support.


Also, if you look at the picture above, you may notice that since the implementation of adapters lies in module E , now this module is forced to implement interfaces from other modules. Thus, we have inverted the direction of the arrow indicating the dependence. We inverted dependencies .


Not all dependencies are worth inverting.


Modules are now less interconnected, which we actually achieved. We did not do this for everything, since changes in other modules for the next couple of years are not expected. Do not worry about changes in what rarely changes. But if you have pieces of the system that change often, or you just don’t know what will be there at the end, it makes sense to protect yourself from possible changes.


For example, if we need a logger, we can always use the PSR\Logger interface because it is standardized, and such things rarely change. Then we can choose any logger that implements this interface to our taste:


Inversion of dependencies between components

As you can see, thanks to this interface, our application still does not depend on a specific logger. Logger depends on this abstraction. But both "modules" do not depend on each other.


Insulation


Interfaces and late binding allow us to "abstract" the implementation of logic from extraneous details. We should try to make the modules as isolated and self-sufficient as possible. When all modules are independent, we get the opportunity and independently develop them. And this may be important from a business point of view.


Often, when it comes to abstractions, people love to bring everything to the extreme, forgetting why it is all necessary from the beginning.


When the project is planned to be supported much longer than the period of support for your framework, it makes sense to wrap all the used things into adapters. This is a kind of extreme, but in such conditions it is justified. We are unlikely to change the framework, but perhaps we would like to update the major version in the future without pain.


Or, for example, another common misconception - abstraction from the repository. The ability to completely replace the database is by no means the goal of implementing this abstraction, it is rather a quality criterion. Instead, we just have to give such an isolation level that our logic does not depend on the capabilities of the database. And this does not mean that we should not use these opportunities.


For example, we implemented a search in our beloved MySQL, but in the end it took a better implementation. And we decided to take ElasticSearch for this, simply because with it search to do faster. We also cannot refuse MySQL, but thanks to the built abstraction, we can add another database in order to more effectively accomplish a specific task.


Or we make another social network, and we need to somehow track the repost. Yes, we can do it on MySQL but it will be inconvenient. This suggests graph databases. And such scenarios of the masses. We should be guided by common sense first and foremost and not dogmas.


This is probably all. I am sure that I did not say everything and there may be questions left, so feel free to ask them in the comments. I’m also sure that I don’t know everything, and I’ll be happy to comment on the topic a little deeper, or life examples, when dependency inversion helped or could help. Well, if you find typos / errors in the article - I will be glad to messages in PM.


')

Source: https://habr.com/ru/post/313796/


All Articles