DIP - Dependency Inversion principle
The Dependency Inversion Principle is the fifth principle in the Solid Design Principles.
- High-Level Modules should not depend on Low-Level Modules. Both Should depend on abstraction.
- Abstraction should not depend on details. Details should depend on Abstraction.
High-Level Modules.
- The High-Level Module contains important policy decisions and business logic.
- They Should not be affected by changes in Low-Level Modules.
Low-Level Modules.
- Low-Level Modules contains implementation details.
- Changes in Low-Level Modules should not affect High-Level Modules.
Abstractions
- Use Interfaces or Abstraction class to create an abstraction Layer.
- High-level modules depend on this abstraction, allowing low-level module changes without affecting high-level logic.
Violating DIP:
class FileLogger {
func log(message: String) {
print("Loggin Message to file: \(message)")
}
}
class UserService {
private let logger = FileLogger()
func createUser(name: String) {
logger.log(message: "User \(name) Created.")
}
}
// usage
let userService = UserService()
userService.createUser(name: "Vinay Kumar")
Issues with Violating DIP:
- Tight Coupling:
- UserService is tightly coupled to FileLogger, making it difficult to change the logging mechanism without modifying UserService.
- Reduced Flexibility:
- Switching to a different logging method (e.g., database logger, cloud logger) requires changing UserService.
- Hard to Test:
- Testing UserService requires an actual FileLogger, making it harder to isolate and test.
Adhering to DIP:
Using Protocol We can adhere to DIP.
protocol Logger {
func log(message: String)
}
class FileLoggerDIP: Logger {
func log(message: String) {
print("Logging message to file: \(message)")
}
}
class ConsoleLogger: Logger {
func log(message: String) {
print("Logging message to console: \(message)")
}
}
class UserServiceDIP {
let logger: Logger
init(logger: Logger) {
self.logger = logger
}
func createUser(name: String) {
self.logger.log(message: "User \(name) Created")
}
}
// usage
let fileLoggerDIP = FileLoggerDIP()
let consoleLogger = ConsoleLogger()
let userServiceDIPUsingFileLogger = UserServiceDIP(logger: fileLoggerDIP)
let userServiceDIPUsingConsoleLogger = UserServiceDIP(logger: consoleLogger)
userServiceDIPUsingFileLogger.createUser(name: "Vinay Kumar DIP FileLogger")
userServiceDIPUsingConsoleLogger.createUser(name: "Vinay Kumar DIP ConsoleLogger")
Benefits of Adhering to DIP:
- Improved Maintainability:
- Changes in low-level modules (like FileLogger) do not affect high-level modules (like UserService).
- Enhanced Flexibility:
- Easily switch between different logging mechanisms without changing the high-level module.
- Increased Testability:
- High-level modules can be tested with mock implementations of abstractions.
Drawbacks of Adhering to DIP:
- Increased Complexity:
- Requires creating additional interfaces or abstract classes.
- More Boilerplate Code:
- More code is needed to set up the abstractions.
Mitigating Drawbacks:
- Balanced Approach:
- Apply DIP where it provides significant benefits, balancing simplicity and extensibility.
- Clear Documentation:
- Maintain clear documentation to help developers understand the dependencies.
- Use of Dependency Injection Frameworks:
- Use frameworks to manage dependencies efficiently.
Conclusion:
You can create more maintainable, understandable, and flexible software by thoughtfully understanding and applying the Dependency Inversion Principle. Ensuring that high-level modules depend on abstractions rather than low-level details promotes better software design and enhances the overall quality of the codebase.
Interface Segregation Principle
Single Responsibility Principle
Check My GitHub Swift Playground Repo.
Top comments (0)