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.
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.
class UserDataAccess:
def get_user(self, user_id):
# Implementation to fetch user from the database
pass
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.
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
.
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.
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
).