← back to blog

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

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:

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:

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: