Inversion: In 4 easy steps
2025-01-01
I'm nearly certain that learning this technique is a total game-changer for your ability to produce well architected code. I knew I needed it a long time ago, but I just didn't know what it was. I've been searching for ways to organise my code so that I stop hitting 'brick walls of complexity'. I always get to a point where I need to abandon a personal project because managing dependancies is becoming too cumbersome, and I know there has to be a better way.
I remember the first time I heard the term 'Dependancy Injection'. It sounded like some highly complex process that I would do well to avoid in my coding adolescence. It wasn't until much later that I found out I had been doing it for years without knowing. Later still, I read the following quote by James Shore:
'Dependency Injection' is a 25-dollar term for a 5-cent concept.
class MusicPlayer {
func play() {
let song = Playlist.shared.nextSong() // Hidden dependency on `Playlist.shared`
print("Now playing: \(song)")
}
}
class Playlist {
static let shared = Playlist()
private var songs = ["Song A", "Song B", "Song C"]
private var currentIndex = 0
func nextSong() -> String {
let song = songs[currentIndex]
currentIndex = (currentIndex + 1) % songs.count
return song
}
}
Issues with the Code
- Tight Coupling: MusicPlayer is tightly coupled to the singleton Playlist.shared.
- Hidden Dependencies: The dependency on Playlist is implicit, making it hard to test MusicPlayer in isolation.
- Global State: Playlist.shared introduces mutable global state, which can lead to unexpected bugs.
Step 1: Introduce Dependency Injection
Make the dependency on Playlist explicit by injecting it into the MusicPlayer:
class MusicPlayer {
private let playlist: Playlist
init(playlist: Playlist) {
self.playlist = playlist
}
func play() {
let song = playlist.nextSong()
print("Now playing: \(song)")
}
}
The Playlist now needs to be passed to MusicPlayer during initialization:
class Playlist {
private var songs = ["Song A", "Song B", "Song C"]
private var currentIndex = 0
func nextSong() -> String {
let song = songs[currentIndex]
currentIndex = (currentIndex + 1) % songs.count
return song
}
}
// Usage
let playlist = Playlist()
let musicPlayer = MusicPlayer(playlist: playlist)
musicPlayer.play()
Benefits:
- MusicPlayer is no longer coupled to the singleton Playlist.shared.
- Dependencies are explicit and testable.
Step 2: Apply the Interface Segregation Principle
Extract a protocol for Playlist to implement so that MusicPlayer depends on an abstraction, not a concrete implementation.
protocol PlaylistProvider {
func nextSong() -> String
}
class Playlist: PlaylistProvider {
private var songs = ["Song A", "Song B", "Song C"]
private var currentIndex = 0
func nextSong() -> String {
let song = songs[currentIndex]
currentIndex = (currentIndex + 1) % songs.count
return song
}
}
class MusicPlayer {
private let playlist: PlaylistProvider
init(playlist: PlaylistProvider) {
self.playlist = playlist
}
func play() {
let song = playlist.nextSong()
print("Now playing: \(song)")
}
}
// Usage
let playlist = Playlist()
let musicPlayer = MusicPlayer(playlist: playlist)
musicPlayer.play()
Benefits:
- MusicPlayer depends only on the PlaylistProvider abstraction, making it more flexible.
- You can now easily substitute Playlist with a mock implementation for testing.
Step 3: Use Inversion of Control (IoC)
Use an IoC container or factory to manage object creation, so your MusicPlayer doesn't need to know how to create its dependencies. Here's a simplified manual IoC:
class MusicApp {
static func createMusicPlayer() -> MusicPlayer {
let playlist = Playlist()
return MusicPlayer(playlist: playlist)
}
}
// Usage
let musicPlayer = MusicApp.createMusicPlayer()
musicPlayer.play()
Step 4: Add Dependency Inversion
In a more complex system, the MusicPlayer might need additional services, such as a SongFetcher that retrieves songs from a network or database. Let's see how to decouple further:
1. Create an abstraction for fetching songs:
protocol SongFetcher {
func fetchNextSong() -> String
}
2. Implement a concrete SongFetcher:
class NetworkSongFetcher: SongFetcher {
func fetchNextSong() -> String {
// Pretend to fetch from a network
return "Song from the cloud"
}
}
3. Update MusicPlayer to depend on the SongFetcher:
class MusicPlayer {
private let songFetcher: SongFetcher
init(songFetcher: SongFetcher) {
self.songFetcher = songFetcher
}
func play() {
let song = songFetcher.fetchNextSong()
print("Now playing: \(song)")
}
}
// Usage
let songFetcher = NetworkSongFetcher()
let musicPlayer = MusicPlayer(songFetcher: songFetcher)
musicPlayer.play()
Benefits:
- MusicPlayer doesn't care whether the songs come from a playlist, a network, or any other source. It simply relies on the SongFetcher abstraction.