This Dependency Inversion Principle is relatively simple to use, but the related concepts are harder to understand. For example, What gets inverted in the Dependency Inversion Principle? What is the difference between “Inversion of Control(IOC)”, “Dependency Injection(DI)” and “Dependency Inversion Principle(DIP)”?
- Introduction to Dependency Inversion Principle
- Implementing DIP in Practice
- What gets inverted in the Dependency Inversion Principle?
- What are the IOC and DI?
Introduction to Dependency Inversion Principle
Understanding Dependency Inversion Principle
The Dependency Inversion Principle is predicated on two key concepts:
- High-level modules should not depend on low-level modules. Both should depend on abstractions.
- Abstractions should not depend on details. Details(concrete implementations) should depend on abstractions.
- Abstractions should not depend on details: This means that abstract interfaces in high-level modules should not be tightly coupled with low-level modules that implement specific functionality. In other words, your high-level business logic should not be directly dependent on the detailed and low-level implementation.
- Details should depend on abstractions: Conversely, the low-level modules or details should be designed based on abstractions. This implies that the implementation of specific functionalities should conform to general abstractions. When changes are made to the low-level details, they do not impact the high-level modules as long as they conform to the same abstractions.
In simpler terms, DIP advocates for a system design where the high-level policy-setting modules are not tightly coupled with the low-level, detail-oriented modules. Instead, both should rely on abstract interfaces.
Why is DIP Important?
Reduces Coupling
- By depending on abstractions rather than concrete classes, DIP reduces the coupling between different parts of the system.
Enhances Flexibility
- It allows for easier changes and additions to the system. Changing the behavior of a system component doesn’t necessitate changes in dependent modules.
Improves Code Reusability
- Abstract interfaces enable different implementations to be interchanged without altering the high-level modules.
Facilitates Testing
- With DIP, it becomes easier to test modules in isolation, as dependencies can be replaced with mocks or stubs.
Implementing DIP in Practice
Consider a scenario in a shopping cart application where a Cart
class directly instantiates a MySQLDatabase
class to store cart data:
1
2
3
4
5
6
7
8
9
public class Cart {
private MySQLDatabase database;
public Cart() {
database = new MySQLDatabase();
}
// Methods using database
}
- This design violates DIP, as
Cart
(a high-level module) is directly dependent onMySQLDatabase
(a low-level module).- In this case,
Cart
andDatabase
are strongly coupled. If you want to switch or add the corresponding database in the future, it may cause a lot of modifications.
- In this case,
Refactoring with DIP
To adhere to DIP, we should introduce an abstract interface and make both high-level and low-level modules depend on this abstraction:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public interface Database {
void store(Object data);
}
public class MySQLDatabase implements Database {
@Override
public void store(Object data) {
// Implementation for MySQL
}
}
public class Cart {
private Database database;
public Cart(Database database) {
this.database = database;
}
// Methods using database
}
- Now,
Cart
depends on theDatabase
interface, an abstraction, rather than a concrete implementation.- In this case, there is not a tight coupling between
Cart
andDatabase
. If you want to switch or add the appropriate database in the future, there is no need to modify theCart
class.
- In this case, there is not a tight coupling between
What gets inverted in the Dependency Inversion Principle?
-
Traditional Dependency Structure: In traditional software architecture, high-level modules depend directly on low-level modules. For instance, a business logic class might directly instantiate and use a data access class. This direct dependency makes the high-level module less reusable and more difficult to test, as it is tightly coupled with the specific implementation details of the lower-level module.
-
Inversion in DIP: DIP inverts the Traditional Dependency Structure. Instead of high-level modules depending on low-level modules, both should depend on abstractions. This means that high-level modules define interface abstractions (representing what they need to function), and low-level modules implement these interfaces.
The term “inversion” in DIP means:
The dependency of the higher-level module on the details of the lower-level module is inverted to a dependency of the lower-level module on the abstractions of the higher-level module. That is, the dependencies on the concrete implementations are inverted to dependencies on abstract interfaces.
What are the IOC and DI?
- IOC (Inversion of Control)
- Inversion of Control is a more general design idea, not a specific implementation, and is generally used to guide design at the framework level.
- The term “control” refers to the control of program execution.
- Before the use of IOC frameworks, the execution of the program was controlled by the programmer. After the framework is used, the execution of the program is controlled by the IOC framework. The control of the execution is “inverted” from the programmer to the framework.
- DI (Dependency Injection)
- In opposition to the IOC, DI is a specific coding technique rather than a general design idea.
- Instead of creating objects of the dependency inside the class by
new()
, we create objects of the dependency externally, and then pass (or inject) them to the class via constructors, method parameters, etc.