
The Dependency Inversion Principle (DIP) is a cornerstone of the SOLID principles in object-oriented design, playing a vital role in architecting robust and maintainable software systems. DIP guides us in decoupling high-level modules from low-level modules by introducing abstractions, promoting flexibility and testability. In the context of Inversion of Control (IoC) and Dependency Injection (DI), DIP brings two crucial principles to the forefront:
- Decoupling High-Level form Low-Level Modules: High-level modules, which often encapsulate the core business logic, should not directly depend on low-level modules responsible for specific implementation details like data access or external services.
- Depend on Abstractions: Both high-level and low-level modules should rely on abstractions, often defined as interfaces or abstract classes, rather than concrete implementations.
In this article, we’ll explore Dependency Inversion and Inversion of Control through Dependency Injection, using TypeScript and Node.js. We’ll break it down step by step, accompanied by code examples.
Define Abstractions (Interfaces or Abstract Classes)
Our journey begins by defining abstractions that represent the dependencies required by high-level modules. These abstractions serve as contracts, specifying the methods and properties that concrete implementations must provide.
// User.ts - Represents the structure of a user
type User = {
name: string;
email: string;
dateOfBirth: Date;
};
// ExportUser.ts - Abstraction for exporting user data
interface ExportUser {
export(user: User): void;
}
Implement Concrete Classes
Implement Concrete Classes With our abstractions in place, we proceed to create concrete implementations that adhere to these abstractions.
// ExportUserToCSV.ts - Concrete implementation for exporting user data to CSV
class ExportUserToCSV implements ExportUser {
export(user: User) {
// Logic to export user to a CSV file
console.log(`Exported ${user.name} to CSV`);
}
}
// ExportUserToPDF.ts - Concrete implementation for exporting user data to PDF
class ExportUserToPDF implements ExportUser {
export(user: User) {
// Logic to export user to a PDF file
console.log(`Exported ${user.name} to PDF`);
}
}
Use Dependency Injection
Our next step is to embrace Dependency Injection (DI), which allows us to inject these dependencies into high-level modules. DI enhances testability and separates concerns by providing dependencies from the outside.
// ExportUserUseCase.ts - A use case class for exporting users
class ExportUserUseCase {
constructor(private exportUser: ExportUser) {}
execute(user: User) {
// Export the user data using the injected dependency
this.exportUser.export(user);
}
}
// Usage examples:
// With CSV
const exportUsertoCSV = new ExportUserToCSV();
const exportUserUseCaseCSV = new ExportUserUseCase(exportUsertoCSV);
exportUserUseCaseCSV.execute({
name: 'Bobson Paebi',
email: 'paebi@bubstack.tech',
dateOfBirth: new Date(),
});
// With PDF
const exportUsertoPDF = new ExportUserToPDF();
const exportUserUseCasePDF = new ExportUserUseCase(exportUsertoPDF);
exportUserUseCasePDF.execute({
name: 'Bobson Paebi',
email: 'paebi@bubstack.tech',
dateOfBirth: new Date(),
});
In this above example:
- We introduced the ExportUserUseCase class, which encapsulates the logic for exporting users. It depends on an ExportUser abstraction and provides an execute method for exporting users.
- Notice that ExportUserUseCasedoes not depend on either ExportUserToPDF or ExportUserToCSV instead it depends on ExportUser which is an abstraction.
By following this approach, you create a clear separation of high-level logic from concrete implementations. This separation facilitates the interchange of export formats and simplifies the process of mocking dependencies for testing. Adhering to the Dependency Inversion Principle allows your application to depend on abstractions.
CONCLUSION:
Understanding and implementing the Dependency Inversion Principle (DIP) is crucial for crafting maintainable and scalable software systems. By decoupling high-level and low-level modules and depending on abstractions, you can achieve a more flexible and testable codebase, whether you’re working with TypeScript, Node.js, or any other modern technology stack.
REFERENCES
- Hexagonal Architecture and Clean Architecture (with examples). Retrieved from: https://www.youtube.com/watch?v=bRl-sTvLbsI&list=PLN3ZW2QI7gLfQ4oEkDWw0DZVIjvAjO140&index=2
- Dependency Inversion. Retrieved from: https://khalilstemmler.com/wiki/dependency-inversion/