โ Back to OOP Concepts
๐
Principles
Dependency Inversion
High-level modules should not depend on low-level modules directly โ both should depend on abstractions.
The Problem
When high-level business logic directly instantiates and depends on concrete low-level classes (e.g., a specific email client), it becomes rigid, hard to test, and tightly coupled to implementation details that may change.
How It Works
- โบBoth high-level and low-level modules depend on shared abstractions (interfaces/protocols)
- โบConcrete implementations are "injected" into high-level modules via constructor or property injection
- โบEnables swapping implementations (e.g., real sender vs. mock) without changing business logic
- โบUnderpins dependency injection frameworks and testable, layered architectures
When to Use
โBusiness logic needs to be testable with mocked dependencies
โSwapping infrastructure (database, network client, file system) without touching domain logic
โDecoupling layers in clean or hexagonal architecture
Real-World Examples
๐
iOS
Injecting a NetworkServiceProtocol into a ViewModel instead of using URLSession directly
๐ค
Android
Hilt/Dagger injecting a Repository interface implementation into a ViewModel
๐
Web
Passing a database interface into a service constructor, swappable with an in-memory implementation for tests
Code Examples
Swift
protocol MessageSender {
func send(_ message: String)
}
struct EmailSender: MessageSender {
func send(_ message: String) { print("Email: \(message)") }
}
struct SMSSender: MessageSender {
func send(_ message: String) { print("SMS: \(message)") }
}
// High-level module depends on the abstraction, not a concrete sender
struct NotificationService {
let sender: MessageSender
func notify(_ message: String) {
sender.send(message)
}
}
let service = NotificationService(sender: SMSSender())
service.notify("Your order has shipped")