Dependency Inversion Principle

Dependency Inversion is one of the core principles of Clean architecture.

But what does dependency inversion mean in practice?

The DI principle establishes two rules:

1. Inner layers should not reference outer layers. But they should depend on abstractions (interfaces).

2. Abstractions should not depend on details. But details (concrete implementations) should depend on abstractions.

The Dependency Inversion Principle (DIP) is one of the five SOLID principles of object-oriented programming, coined by Robert C. Martin. DIP states that:

  1. High-level modules should not depend on low-level modules. Both should depend on abstractions.
  2. Abstractions should not depend on details. Details should depend on abstractions.

In simpler terms, DIP suggests that classes should depend on abstractions rather than concrete implementations, and high-level modules should not be tightly coupled to low-level implementation details.

Here’s a breakdown of the key concepts of DIP:

  1. High-level modules: These are modules or components that orchestrate the application’s business logic or workflow. They should be designed in a way that makes them independent of specific implementations of lower-level components.
  2. Low-level modules: These are modules that handle specific tasks or details within the system. They should be designed to be easily interchangeable without affecting the high-level modules.
  3. Abstractions: These are interfaces or abstract classes that define a contract for functionality. High-level modules should depend on these abstractions rather than concrete implementations. This allows for flexibility and easier substitution of different implementations.
  4. Details: Details refer to concrete implementations of functionality. According to DIP, these implementations should depend on abstractions, not the other way around. This decouples the high-level modules from specific implementation details, making the system more flexible and easier to maintain.

In practice, DIP can be achieved through techniques such as dependency injection, where dependencies are provided to a class from the outside rather than being created internally. This allows for easier substitution of dependencies and promotes loose coupling between components.

Overall, adhering to the Dependency Inversion Principle helps to create more modular, flexible, and maintainable codebases by reducing coupling and promoting abstraction and decoupling.

Let’s consider example in the context of Android development. Suppose we have an application that fetches weather data from different sources (e.g., online API or local database) and displays it to the user. We’ll demonstrate how to apply the Dependency Inversion Principle (DIP) by decoupling the high-level modules from low-level implementations using dependency injection.

Without Dependency Inversion Principle:

// High-level module
public class WeatherApp {
    private OnlineWeatherService onlineWeatherService = new OnlineWeatherService(); // High-level module depends on low-level OnlineWeatherService

    public void fetchAndDisplayWeather() {
        WeatherData weatherData = onlineWeatherService.fetchWeather();
        // Code to display weather data to the user
    }
}

// Low-level module
public class OnlineWeatherService {
    public WeatherData fetchWeather() {
        // Code to fetch weather data from online API
        return new WeatherData();
    }
}

In this example, the WeatherApp class directly depends on the OnlineWeatherService class, violating the Dependency Inversion Principle. If we want to switch to a different weather data source, such as a local database or another API, we would need to modify the WeatherApp class, leading to tight coupling and code modification.

With Dependency Inversion Principle:

// Abstraction
public interface WeatherService {
    WeatherData fetchWeather();
}

// Low-level module implementing the abstraction
public class OnlineWeatherService implements WeatherService {
    @Override
    public WeatherData fetchWeather() {
        // Code to fetch weather data from online API
        return new WeatherData();
    }
}

// High-level module depending on abstraction, not on specific implementation
public class WeatherApp {
    private final WeatherService weatherService; // High-level module depends on abstraction

    public WeatherApp(WeatherService weatherService) {
        this.weatherService = weatherService;
    }

    public void fetchAndDisplayWeather() {
        WeatherData weatherData = weatherService.fetchWeather();
        // Code to display weather data to the user
    }
}

In this modified example:

  • We introduce a WeatherService interface as an abstraction.
  • The OnlineWeatherService class implements the WeatherService interface, providing the concrete implementation for fetching weather data from an online API.
  • The WeatherApp class depends on the WeatherService interface, not on the OnlineWeatherService class directly. This adheres to the Dependency Inversion Principle, as the high-level module depends on an abstraction rather than a specific implementation.

With this design, we can easily swap out the implementation of WeatherService (e.g., for fetching weather data from a local database) without modifying the WeatherApp class. This promotes loose coupling and makes the codebase more maintainable and flexible, adhering to the principles of the Dependency Inversion Principle.