Dependency Inversion Principle: Building Flexible and Maintainable Software Designs!

Reza Erami
3 min readOct 23, 2023

--

The Dependency Inversion Principle (DIP) suggests that high-level modules should not have direct dependencies on low-level modules. Instead, both high-level and low-level modules should depend on abstractions or interfaces. Furthermore, abstractions should not rely on implementation details; rather, implementation details should depend on abstractions. By adhering to this principle, the risk of unintended side effects in high-level modules caused by changes in low-level modules is minimized. With the introduction of an abstract layer, dependencies are inverted, reducing the traditional top-down dependency structure.

Let’s examine an example involving a “UserService” and “RoleRepository” class. In this scenario, the “UserService” includes an implemented “getRole” method to retrieve the role of a user.

class UserService extends BaseService {
constructor(repository: UserRepository) {
this.repository = repository;
}
}

In the previous examples, the “UserService” implemented the “BaseService”. However, to add a new method called “getRole”, we will include it in the “UserService” to retrieve the user’s role. Additionally, within the “getRole” method, we will create an instance of “RoleRepository” to access the “RoleRepository”.

class UserService extends BaseService {
constructor(repository: UserRepository) {
this.repository = repository;
}

public async getRole(userId: number): Promise<Role> {
const roleRepository = new RoleRepository();
const user = await this.get(userId);
const role = await roleRepository.findById(user.roleId);
return role;
}
}

With the current approach, we can achieve our desired functionality, but we are also violating the fifth principle of SOLID, which is Dependency Inversion. This is because our “UserService ” is dependent on the details of the “RoleRepository” and creates an instance of it.

To address this issue, we need to inject the “RoleRepository” as a dependency into the “UserService” instead of creating a class instance within it. Furthermore, since “RoleRepository” implements “BaseRepository”, which is an abstract class, we can ensure that the get method exists in that abstract class. This approach helps us avoid relying on implementation details and promotes better adherence to the Dependency Inversion Principle.

class UserService extends BaseService {
private roleRepository: RoleRepository;

constructor(repository: UserRepository, roleRepository: RoleRepository) {
this.repository = repository;
this.roleRepository = roleRepository;
}

public async getRole(userId: number): Promise<Role> {
const user = await this.get(userId);
const role = await this.roleRepository.findById(user.roleId);
return role;
}
}

In the refactored code, we can easily replace “RoleRepository” with any other repository that handles roles. An improvement in this code is that, instead of using the “RoleRepository” class, we can utilize the “RoleService”. This approach keeps the “RoleRepository” layer isolated within the “RolesModule”, promoting a more modular and maintainable design.

class UserService extends BaseService {
private roleService: RoleService;

constructor(repository: UserRepository, roleService: RoleService) {
this.repository = repository;
this.roleService = roleService;
}

public async getRole(userId: number): Promise<Role> {
const user = await this.get(userId);
const role = await this.roleService.get(user.roleId);
return role;
}
}

Previous Episode: Interface Segregation Principle: Flexible Interfaces for Specific Needs

Next Episode: Concluding the Understanding of the SOLID Principles (Coming soon…)

--

--