Dependency Inversion Principle – Clean Mobile Architecture by Petros Efthymiou

Alyaa TalaatAlyaa Talaat
4 min read

The Dependency Inversion Principle (DIP) is a foundational concept in clean architecture that helps build flexible, scalable systems. It promotes creating high-level modules independent of low-level details, allowing for easy updates and better control over dependencies.


What is the Dependency Inversion Principle?

Definition: The Dependency Inversion Principle states that high-level modules should not depend on low-level modules. Instead, both should depend on abstractions, and abstractions should not depend on details; details should depend on abstractions.

This principle is crucial for mobile applications, where keeping modules independent and well-structured can minimize complexity and enable easy integration of new features. DIP helps developers separate high-level business logic from low-level implementation details, allowing them to evolve independently.


Problem Scenario

Suppose you’re developing a mobile app that connects to multiple data sources, such as a remote API and a local database, for storing and retrieving user information. If your high-level business logic depends directly on specific classes for API and database handling, adding or changing a data source would require modifying the core application code, increasing the risk of errors and making maintenance more challenging.


Applying the Dependency Inversion Principle

Let’s explore how the Dependency Inversion Principle can be applied to a mobile application’s data layer to ensure modularity and maintainability using the Clean Mobile Architecture approach.

Step 1: Define Abstractions

To avoid direct dependency on specific data sources, create an interface that defines the methods required for data handling. This allows high-level modules to rely on abstractions instead of concrete classes.

public interface UserDataSource {
    void saveUser(User user);
    User fetchUser(String userId);
}

Step 2: Implement the Abstractions for Each Data Source

Create concrete classes that implement the UserDataSource interface. These classes handle the specifics of each data source, such as a remote API or a local database.

public class ApiUserDataSource implements UserDataSource {
    @Override
    public void saveUser(User user) {
        System.out.println("Saving user data to remote API...");
        // Logic to save user data to remote API
    }

    @Override
    public User fetchUser(String userId) {
        System.out.println("Fetching user data from remote API...");
        // Logic to fetch user data from remote API
        return new User(userId, "API User");
    }
}

public class DatabaseUserDataSource implements UserDataSource {
    @Override
    public void saveUser(User user) {
        System.out.println("Saving user data to local database...");
        // Logic to save user data to local database
    }

    @Override
    public User fetchUser(String userId) {
        System.out.println("Fetching user data from local database...");
        // Logic to fetch user data from local database
        return new User(userId, "Database User");
    }
}

Step 3: Inject the Dependency into the High-Level Module

By using dependency injection, we can pass the appropriate UserDataSource implementation to the business logic without hardcoding dependencies. This makes it easy to switch data sources as needed.

public class UserService {
    private UserDataSource userDataSource;

    public UserService(UserDataSource userDataSource) {
        this.userDataSource = userDataSource;
    }

    public void saveUser(User user) {
        userDataSource.saveUser(user);
    }

    public User getUser(String userId) {
        return userDataSource.fetchUser(userId);
    }
}

Step 4: Client Code

The client code can now switch between different data sources by simply passing a different UserDataSource implementation to UserService, enabling flexibility and ease of testing.

public class DependencyInversionTest {
    public static void main(String[] args) {
        User user = new User("123", "John Doe");

        // Using API as the data source
        UserDataSource apiDataSource = new ApiUserDataSource();
        UserService apiUserService = new UserService(apiDataSource);
        apiUserService.saveUser(user);
        apiUserService.getUser(user.getId());

        // Switching to local database as the data source
        UserDataSource dbDataSource = new DatabaseUserDataSource();
        UserService dbUserService = new UserService(dbDataSource);
        dbUserService.saveUser(user);
        dbUserService.getUser(user.getId());
    }
}

Output:

Saving user data to remote API...
Fetching user data from remote API...
Saving user data to local database...
Fetching user data from local database...

This setup ensures that UserService is not tied to a specific data source, allowing easy substitution or addition of new data sources without modifying the service itself.


Benefits of the Dependency Inversion Principle

  • Reduced Coupling: High-level modules are decoupled from low-level implementations, leading to a more flexible structure.

  • Ease of Testing: Mock implementations of interfaces can be injected during testing, enabling isolated testing of high-level modules.

  • Increased Flexibility: New dependencies can be introduced without modifying existing code, supporting extensibility and adaptability.

  • Enhanced Code Reusability: By separating abstractions from concrete implementations, code can be reused across different projects or modules.


Bullet Points

  • The Dependency Inversion Principle encourages high-level modules to depend on abstractions, reducing coupling to specific implementations.

  • DIP promotes clean architecture by isolating core business logic from detailed implementations, making code more resilient to changes.

  • Implementing DIP with dependency injection provides flexibility, allowing you to swap implementations without modifying the core functionality.

  • Following DIP supports clean and maintainable codebases that can adapt to new requirements with minimal disruption.


Conclusion

The Dependency Inversion Principle is central to creating flexible and maintainable mobile applications. By adhering to DIP, you make it possible to decouple high-level logic from low-level implementations, ensuring your code remains modular, adaptable, and scalable. As your application evolves, the principles of dependency inversion will keep it robust and open to future expansions.


0
Subscribe to my newsletter

Read articles from Alyaa Talaat directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Alyaa Talaat
Alyaa Talaat

As a continuous learner, I’m always exploring new technologies and best practices to enhance my skills in software development. I enjoy tackling complex coding challenges, whether it's optimizing performance, implementing new features, or debugging intricate issues.