2022-05-19

Dependency Inversion Principle

What is Dependency Inversion Principle (DIP)

The Dependency Inversion Principle (DIP) is about the relationships and dependencies between high-level and low-level modules within a software system. It postulates two primary points:

  • High-level modules should not depend on low-level modules. Both should depend on abstractions.
    High-level modules encapsulate the business rules of a system - the critical, complex logic that gives the software its unique value. Low-level modules, on the other hand, handle operations such as database access, networking, I/O operations, etc. With DIP, both types of modules interface through abstractions (like interfaces or abstract classes), ensuring that any changes to the details of the low-level modules won't impact the high-level ones, thus making the code more robust and easier to maintain.

  • Abstractions should not depend upon details. Details should depend upon abstractions.
    Traditionally, abstractions (interfaces or abstract classes) are designed around the details (concrete implementations). DIP inverts this relationship. The concrete implementations should be constructed based on the abstractions. This approach leads to a more flexible system, where new functionality can be introduced by simply creating new implementations of the abstraction, without modifying existing code.

DIP
Dependency Inversion Principle(DIP)

Dependency Inversion in DDD

Domain-Driven Design (DDD) is an approach to software development that emphasizes collaboration between technical and domain experts to meet complex business needs. One of the central aspects of DDD is a clean separation between the domain layer and the infrastructure layer. In this chapter, I explore how the Dependency Inversion Principle (DIP) plays a pivotal role in ensuring this separation and creating robust, maintainable software solutions.

Role of Domain and Infrastructure Layers in DDD

Before discussing DIP in the context of DDD, it's essential to understand the roles of the domain and infrastructure layers in DDD:

  • Domain Layer
    This layer encapsulates the business logic and rules of the system. It is considered the heart of the software and is completely independent of any specific technology or infrastructure concerns.

  • Infrastructure Layer
    This layer includes elements such as databases, UI, external services, etc. These elements are responsible for technical concerns that support the functioning of the domain layer but are not related to the business logic itself.

In DDD, it is crucial to maintain a clear separation between these layers to ensure that changes in one layer do not impact the other, providing a maintainable and flexible architecture.

Applying Dependency Inversion Principle in DDD

The Dependency Inversion Principle can be a powerful tool to maintain the separation between the domain layer and the infrastructure layer, ensuring that the domain layer remains isolated from changes in infrastructure.

In line with DIP, the high-level domain layer should not depend on the low-level infrastructure layer. Instead, both should depend on abstractions. Let's take a deeper look at how this is accomplished:

  • Dependencies on Abstractions
    In a DDD context, the domain layer defines interfaces (abstractions) for any services it needs from the infrastructure layer. These services could include repositories for persisting domain objects, services for sending emails, etc.

  • Inverting the Dependencies
    The infrastructure layer provides concrete implementations for these interfaces, but crucially, the domain layer remains ignorant of these implementations. Thus, the direction of the dependency is inverted: rather than the domain layer depending on the infrastructure layer, it is the infrastructure layer that depends on the domain layer.

Implementing the DIP in DDD

In this chapter, I will look at a Python implementation that demonstrates the Dependency Inversion Principle (DIP) in a Domain-Driven Design (DDD) context.

For our demonstration, let's consider a simple system where a UserService in the domain layer requires UserDataAccess from the infrastructure layer to retrieve user information from a database.

Non-Dependency Inversion Case

First, let's look at a situation where DIP is not implemented.

infrastructure_layer.py
class UserDataAccess:
    def get_user(self, user_id):
        # Implementation to fetch user from the database
        pass
domain_layer.py
from infrastructure_layer import UserDataAccess

class UserService:
    def __init__(self):
        self.user_data_access = UserDataAccess()

    def get_user_details(self, user_id):
        return self.user_data_access.get_user(user_id)

In this example, the UserService class directly depends on the UserDataAccess class from the infrastructure layer. This design violates DIP because the high-level module UserService depends directly on the low-level module UserDataAccess.

Implementing Dependency Inversion

Now, let's modify the above system to comply with DIP.

domain_layer.py
from abc import ABC, abstractmethod

class IUserRepository(ABC):
    @abstractmethod
    def get_user(self, user_id):
        pass

class UserService:
    def __init__(self, user_repository: IUserRepository):
        self.user_repository = user_repository

    def get_user_details(self, user_id):
        return self.user_repository.get_user(user_id)

In this updated example, we have defined an interface IUserRepository in the domain layer. This interface provides an abstract method get_user, which the UserService will use to fetch user data.

The UserService now depends on the IUserRepository interface, rather than the concrete UserDataAccess class. When creating a UserService object, we pass an object of a class that implements IUserRepository.

infrastructure_layer.py
from domain_layer import IUserRepository

class UserDataAccess(IUserRepository):
    def get_user(self, user_id):
        # Implementation to fetch user from the database
        pass

In the infrastructure layer, UserDataAccess now implements the IUserRepository interface. By doing so, UserDataAccess conforms to the abstraction that UserService depends upon.

main.py
from domain_layer import UserService
from infrastructure_layer import UserDataAccess

def main():
    user_repository = UserDataAccess()
    user_service = UserService(user_repository)
    user_service.get_user_details("user_id")

if __name__ == "__main__":
    main()

In the main part of the application, we create an instance of UserDataAccess and pass it to UserService. This design aligns with the DIP: high-level modules (UserService) do not depend on low-level modules (UserDataAccess); both depend on abstractions (IUserRepository).

Ryusei Kakujo

researchgatelinkedingithub

Focusing on data science for mobility

Bench Press 100kg!