Every time we create an object within a function, the function's dependency on that object's class appears. In other words, functions work in tandem with specific classes.
There is a formal way to check how much your code is tied up. Take any function and imagine you're transferring it to another project. How many dependencies do functions entail? How many functions entail their dependencies?
If moving a function requires moving a lot of code, then the code has high cohesion.
There is a term for code decoupling — Dependency Inversion Principle. It's also known as DIP from SOLID. Here's its wording:
- Upper-level modules must not depend on lower-level modules. Both types of modules must depend on abstractions
- Abstractions should not depend on details. The details must depend on abstractions
Depending on the language, these phrases have slightly different meanings. By and large, you don't need to get caught up in a particular class implementation. Creating objects where we use them binds us to the class of those objects without us being able to change them.
The correct approach is to invert dependencies. That way, we do not work with classes directly. We get objects from the desired classes from outside — for example, through function parameters.
The DIP talks about binding to interfaces instead of classes in function signatures. We will discuss it later when we finish the basic concepts.
Before:
const doSomething = () => {
const logger = new Logger();
// Some code
};
After:
const doSomething = (logger) => {
// Some code
};
Speaking of DIP, people like to use the Hollywood principle as an analogy: "Don't call us, we will call you".
It means that you don't need to use classes directly. Instead, you can get ready-made objects as an external dependency.
Do we always have to stick to this principle? Any code built entirely in this style becomes excessively abstract and hard to understand. There are no magic wands you can wave in programming. Every time you have to look at the conditions to solve the problem. If we need to change our implementation, we do so. If not, we work directly.
When we discuss dependency inversion, dependency injection is not far behind. While the DIP is about modularity, dependency injection shows specific ways to achieve it. About how you can pass dependencies to code that uses them. There are a total of three ways to inject dependencies:
Pass them as arguments to functions or methods. It is the method we've used so far:
doSomethingUseful(new Logger());
Via a constructor in situations where we use objects:
const app = new Application(new Logger());
Via setters:
const app = new Application(); app.setLogger(new Logger());
It is best not to use this method if possible. It has to do with changing objects and breaking integrity(more details in our course on object-oriented design).
As you can see, behind this fancy term we have one very simple thing – parameter transfer. However, these terms allow you to understand more meaning without having to know additional context. The main thing is not to get carried away, or you'll end up as an architectural astronaut.
Recommended materials
Are there any more questions? Ask them in the Discussion section.
The Hexlet support team or other students will answer you.
For full access to the course you need a professional subscription.
A professional subscription will give you full access to all Hexlet courses, projects and lifetime access to the theory of lessons learned. You can cancel your subscription at any time.