Introduction to SOLID principles: Dependency Inversion Principle
Learn about DIP in an intuitive way
The SOLID principles are five principles related to Object-Oriented
Programming (OOP for short). The purpose of these principles is to
help us write software that is easy to test, maintain and extend.
The Dependency Inversion Principle (DIP for short) is one of the SOLID
principles. It's actually the last one, corresponding to the letter D.
Let's learn about DIP and what benefits it brings.
The Dependency Inversion Principle states that high level modules
should not depend on low level modules. Furthermore, it states that
both hight level and low level modules should depend on abstractions.
Let's break down the previous statements.
What are modules?
Modules are software components. Since the
SOLID principles relate to OOP, we can consider modules to be classes.
This does not mean that modules strictly equal classes, we can
consider a function to be a module as well.
What are high level and low level modules?
Hight level modules
are ones that contain the business logic of the software. Low level
modules are ones that take care of the technical needs of the software
(e.g providing a REST API, accessing storage, sending emails).
What does it mean for high level modules to depend on low level
modules?
It means that high level modules have no control over
the APIs of low level modules. It also means that low level modules
are not tailored to fit high level modules. It's the other way around,
high level modules are tailored to fit low level modules. Since high
level modules contain business logic and low level modules take care
of technical details, it means that business logic is tailored to fit
technical details. This causes software to be harder to test, maintain
and extend.
What does it mean for high level modules and low level modules to
depend on abstractions?
It means that high level modules and low level modules need to have
"contracts" between them. The "rules" in the "contracts" are given by
the high level modules and it's the low level modules' job to satisfy
those rules. These "contracts" invert the dependency and make low
level modules dependent on high level modules. As a result, the
technical details are tailored to fit the business logic.
How is the Dependency Inversion Principle applied in reality? Here is an example where DIP is not applied.
// low level module
class OrderRepository {
private DatabaseConnection databaseConnection;
public OrderRepository(DatabaseConnection databaseConnection) {
this.databaseConnection = databaseConnection;
}
public void insertOrder(Order order) {
databaseConnection.table("order").insert(order);
}
public void updateOrder(int orderId, Order order) {
databaseConnection.table("order").updateById(orderId, order);
}
public void deleteOrder(int orderId) {
databaseConnection.table("order").deleteById(orderId);
}
}
// high level module
class CreateOrderService {
private OrderRepository orderRepository;
public CreateOrderService(OrderRepository orderRepository) {
this.orderRepository = orderRepository;
}
public void createOrder(OrderItem[] items) {
Order order = new Order(items);
this.orderRepository.insertOrder(order);
}
}
The CreateOrderService is the high level module and the OrderRepository is the low level module. The high level module depends on the low level module. There is no "contract" between them. Let's see the same example with DIP applied.
// "contract" between high level and low level module
interface OrderPersister {
void insertOrder(OrderItem[] items);
}
// low level module
class CreateOrderRepository implements OrderPersister {
private DatabaseConnection databaseConnection;
public OrderRepository(DatabaseConnection databaseConnection) {
this.databaseConnection = databaseConnection;
}
public void insertOrder(Order order) {
databaseConnection.table("order").insert(order);
}
}
// high level module
class CreateOrderService {
private OrderPersister orderPersister;
public CreateOrderService(OrderPersister orderPersister) {
this.orderPersister = orderPersister;
}
public void createOrder(OrderItem[] items) {
Order order = new Order(items);
this.orderPersister.insertOrder(order);
}
}
The OrderPersister interface is the "contract" between the high level module and low level module. The interface contains definitions of what the high level module needs and the low level module provides the implementations for those needs. With the Dependency Inversion Principle applied, the business logic is in control of the technical details. As a result the software is easier to test, maintain and extend.
If you enjoy my articles, please consider supporting me.