Dependency Inversion Principle (DIP)
The Dependency Inversion Principle (DIP) is one of the SOLID principles in object-oriented design. It states that high-level modules should not depend on low-level modules. Both should depend on abstractions, meaning that we should use interfaces or abstract classes to decouple our systems.
Understanding the Dependency Inversion Principle​
Dependency Inversion Principle (DIP): High-level modules, which provide complex logic, should not depend on low-level modules, which provide utility features. Instead, both should depend on abstractions (interfaces or abstract classes). This allows for flexibility and ease of maintenance.
Without Dependency Inversion (Bad Approach)​
Let's consider an example where the Dependency Inversion Principle is violated:
public class ProductSQLRepository {
public void addProduct() {
System.out.println("Product is added to SQL DB");
}
public void getProduct() {
System.out.println("Product is fetched from SQL DB");
}
}
public class ProductService {
ProductSQLRepository repository;
public ProductService(ProductSQLRepository repository) {
this.repository = repository;
}
public void addProduct() {
repository.addProduct();
}
public void getProduct() {
repository.getProduct();
}
}
In this scenario, ProductService
is tightly coupled with ProductSQLRepository
. If we decide to switch from a SQL database to another database like MongoDB, we would need to modify the ProductService
class, making our code less flexible and harder to maintain.
Higher Level Module (Service Class) should not depend on Lower Level Module (Repository Class); both should depend on abstractions like interfaces.
With Dependency Inversion (Good Approach)​
To follow the Dependency Inversion Principle, we introduce an interface, IRepository
, which ProductRepository
implements. This way, ProductService
depends on an abstraction rather than a concrete implementation.
Defining the Interface​
public interface IRepository {
void addProduct();
void getProduct();
}
Implementing the Interface​
public class ProductFakeRepository implements IRepository {
@Override
public void addProduct() {
System.out.println("Product is added to Fake DB");
}
@Override
public void getProduct() {
System.out.println("Product is fetched from Fake DB");
}
}
public class ProductMongoRepository implements IRepository {
@Override
public void addProduct() {
System.out.println("Product is added to Mongo DB");
}
@Override
public void getProduct() {
System.out.println("Product is fetched from Mongo DB");
}
}
public class ProductSQLRepository implements IRepository {
@Override
public void addProduct() {
System.out.println("Product is added to SQL DB");
}
@Override
public void getProduct() {
System.out.println("Product is fetched from SQL DB");
}
}
Using the Interface in ProductService​
public class ProductService {
IRepository repository;
public ProductService(IRepository repository) {
this.repository = repository;
}
public void addProduct() {
repository.addProduct();
}
public void getProduct() {
repository.getProduct();
}
}
Main Class Example​
public class Main {
public static void main(String[] args) {
ProductService sqlProductService = new ProductService(new ProductSQLRepository());
ProductService mongoProductService = new ProductService(new ProductMongoRepository());
sqlProductService.addProduct();
sqlProductService.getProduct();
mongoProductService.addProduct();
mongoProductService.getProduct();
}
}
Benefits of Dependency Inversion​
-
Decoupling: By depending on abstractions rather than concrete implementations, the code becomes more modular and decoupled.
-
Flexibility: It becomes easier to switch out one implementation for another without affecting the rest of the codebase.
-
Maintainability: The code is easier to maintain and extend, as changes in low-level modules do not ripple up to high-level modules.