☰ Menu     Home » Design Principles » Dependency Inversion Principle

Dependency Inversion Principle

The Dependency Inversion Principle (DIP) is a fundamental concept in object-oriented software design that enhances flexibility and maintainability. In simple terms, DIP dictates that high-level modules (handling complex logic) should not depend on low-level modules (performing basic operations), but both should rely on abstractions.

In practical terms, the Dependency Inversion Principle (DIP) states that abstractions should not depend on details, but details should depend on abstractions, thereby inverting the conventional dependency relationship. DIP minimizes coupling by introducing an abstraction layer, leading to code that is easier to modify and extend. Dependency injection, a key aspect of DIP, involves supplying external dependencies to a component, and it can be implemented through constructor, method, or property injection.

Motivation

When we design software applications we can consider the low level classes the classes which implement basic and primary operations(disk access, network protocols,...) and high level classes the classes which encapsulate complex logic(business flows, ...). The last ones rely on the low level classes. A natural way of implementing such structures would be to write low level classes and once we have them to write the complex high level classes. Since high level classes are defined in terms of others this seems the logical way to do it. But this is not a flexible design. What happens if we need to replace a low level class?

Let's take the classical example of a copy module which reads characters from the keyboard and writes them to the printer device. The high level class containing the logic is the Copy class. The low level classes are KeyboardReader and PrinterWriter.

In a bad design the high level class uses directly and depends heavily on the low level classes. In such a case if we want to change the design to direct the output to a new FileWriter class we have to make changes in the Copy class. (Let's assume that it is a very complex class, with a lot of logic and really hard to test).

In order to avoid such problems we can introduce an abstraction layer between high level classes and low level classes. Since the high level modules contain the complex logic they should not depend on the low level modules so the new abstraction layer should not be created based on low level modules. Low level modules are to be created based on the abstraction layer.

According to this principle the way of designing a class structure is to start from high level modules to the low level modules:
High Level Classes --> Abstraction Layer --> Low Level Classes

Intent

  • High-level modules should not depend on low-level modules. Both should depend on abstractions.
  • Abstractions should not depend on details. Details should depend on abstractions.
Dependency Inversion Principle

Dependency Injection Example

Below is an example which violates the Dependency Inversion Principle. We have the manager class which is a high level class, and the low level class called Worker. We need to add a new module to our application to model the changes in the company structure determined by the employment of new specialized workers. We created a new class SuperWorker for this.

Let's assume the Manager class is quite complex, containing very complex logic. And now we have to change it in order to introduce the new SuperWorker. Let's see the disadvantages:

  • we have to change the Manager class (remember it is a complex one and this will involve time and effort to make the changes).
  • some of the current functionality from the manager class might be affected.
  • the unit testing should be redone.

All those problems could take a lot of time to be solved and they might induce new errors in the old functionlity. The situation would be different if the application had been designed following the Dependency Inversion Principle. It means we design the manager class, an IWorker interface and the Worker class implementing the IWorker interface. When we need to add the SuperWorker class all we have to do is implement the IWorker interface for it. No additional changes in the existing classes.


// Dependency Inversion Principle - Bad example
class Worker {

  public void work() {
    // ....working
  }
}

class Manager {

  Worker worker;

  public void setWorker(Worker w) {
    worker = w;
  }

  public void manage() {
    worker.work();
  }
}

class SuperWorker {
  public void work() {
    //.... working much more
  }
}

Below is the code which supports the Dependency Inversion Principle. In this new design a new abstraction layer is added through the IWorker Interface. Now the problems from the above code are solved(considering there is no change in the high level logic):

  • Manager class doesn't require changes when adding SuperWorkers.
  • Minimized risk to affect old functionality present in Manager class since we don't change it.
  • No need to redo the unit testing for Manager class.

// Dependency Inversion Principle - Good example
interface IWorker {
  public void work();
}

class Worker implements IWorker{
  public void work() {
    // ....working
  }
}

class SuperWorker  implements IWorker{
  public void work() {
    //.... working much more
  }
}

class Manager {
  IWorker worker;

  public void setWorker(IWorker w) {
    worker = w;
  }

  public void manage() {
    worker.work();
  }
}

Types of Dependency Injection

The main intent of the depedency injection can be achieved in a few ways:

Constructor Injection

We can inject the depedency to the main component direcly in the constructor when the component is instantiated. In this case the dependency should be already instantiated when the constructor of the main component is invoked.

public class UserService {
  private final Database database;

  public UserService(Database database) {
    this.database = database;
  }

  public User getUser(int id) {
    return database.query("SELECT * FROM users WHERE id = " + id);
  }
}

Method Injection

Method injection is similar to Constructor Injection, but we inject the dependencies in methods. This concept is less likely used in practice, but we might have to use it in certain situations.

public class UserController {
  public User getUser(int id, UserService userService) {
    return userService.getUser(id);
  }

  public static void main(String[] args) {

  UserService userService = new UserService();

  UserController userController = new UserController();

  User user = userController.getUser(123, userService);
    // Do something with the user...
  }
}

Propery Injection

This type of dependency injection consists in passing the dependency through a property. It allows a lazy loading initialization of the dependency, in case the dependency is not instantiated when main component is instantiated. Further more, it makes the code less coupled, since the constructor of the main component does not have to know anything about the dependency. This is useful when the component is instantiated using a factory pattern mechanism. In this case we want to avoid having the factory being dependent on the dependency we want to inject.

public class OrderService {
  // Dependencies assigned directly
  private ProductService productService;

  public OrderService() {
    // ...
  }

  public void setProductService(ProductService productService) {
    this.productService = productService;
  }

  // ...
}

Applicability of Dependency Inversion Principle

Incorporating the Dependency Inversion Principle into your software design can yield numerous benefits in terms of flexibility, maintainability, and testability. By creating an abstraction layer between high-level and low-level modules, you establish a clear separation of concerns and reduce the tight coupling between different parts of your application. This separation allows for easier modifications and extensions without affecting existing code.

Testability and Mocking

One of the significant advantages of adhering to the Dependency Inversion Principle is the improved testability of your code. With high-level modules depending on abstractions, it becomes much simpler to substitute real implementations with mock or stub objects during testing. This facilitates isolated unit testing, as you can easily create mock objects that simulate the behavior of the low-level modules, enabling thorough testing of your high-level logic without relying on external systems or resources.

Inversion of Control (IoC) Containers

The Dependency Inversion Principle aligns well with the concept of Inversion of Control (IoC). IoC containers are tools that manage the instantiation and wiring of objects in your application. By using an IoC container, you can centralize the configuration of your dependencies, making it easier to swap out implementations and manage the overall structure of your application. This approach complements the Dependency Inversion Principle by providing a systematic way to achieve loose coupling and manage the flow of control.

Maintainability and Extensibility

As your software evolves, new requirements and features may necessitate changes to your codebase. By adhering to the Dependency Inversion Principle, you create a foundation for more maintainable and extensible code. When you introduce new functionality, you can focus on creating new high-level classes and abstractions without needing to modify existing components. This reduces the risk of unintended side effects and promotes a modular and scalable architecture.

Real-World Examples

The Dependency Inversion Principle is prevalent in modern software development, and its benefits can be observed in various design patterns and frameworks. For instance, many popular frameworks, such as Spring for Java and Angular for TypeScript/JavaScript, heavily emphasize the use of dependency injection and abstractions. These frameworks provide tools and conventions that facilitate the application of the Dependency Inversion Principle, resulting in more manageable and adaptable codebases.

SOLID Principles

Remember that the Dependency Inversion Principle is just one of the SOLID principles, a set of five design principles that guide the development of well-structured and maintainable software. When used in conjunction with the other SOLID principles (Single Responsibility Principle, Open/Closed Principle, Liskov Substitution Principle, and Interface Segregation Principle), the Dependency Inversion Principle can contribute to the creation of software that is both robust and resilient to change.

Conclusion

When this principle is applied it means the high level classes are not working directly with low level classes, they are using interfaces as an abstract layer. In this case instantiation of new low level objects inside the high level classes(if necessary) can not be done using the operator new. Instead, some of the Creational design patterns can be used, such as Factory Method, Abstract Factory, Prototype.

The Template Design Pattern is an example where the DIP principle is applied.

Of course, using this principle implies an increased effort, will result in more classes and interfaces to maintain, in a few words in more complex code, but more flexible. This principle should not be applied blindly for every class or every module. If we have a class functionality that is more likely to remain unchanged in the future there is not need to apply this principle.