โ† 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")