Simple Swift dependency injection with functions

Dependency injection is a great technique for decoupling code and making it easier to test. Instead of having your objects create their own dependencies on their own, you inject them from the outside, enabling you to set them up differently for various situations — for example in production vs in a test.

Most of the time, we use protocols to enable dependency injection in Swift. For example, let’s say we’re writing a simple card game, where we use a randomizer to draw a random card from a deck, like this:

class CardGame {
    private let deck: Deck
    private let randomizer: Randomizer

    init(deck: Deck, randomizer: Randomizer = DefaultRandomizer()) {
        self.deck = deck
        self.randomizer = randomizer
    }

    func drawRandomCard() -> Card {
        let index = randomizer.randomNumber(upperBound: deck.count)
        let card = deck[index]
        return card
    }
}

In the above example, you can see that we inject an implementation of Randomizer in CardGame’s initializer, which we then use to generate a random index when drawing. In order to make our API easier to use, we also use a default argument — DefaultRandomizer — if no explicit randomizer is given. Here is what that protocol & default implementation look like:

protocol Randomizer {
    func randomNumber(upperBound: UInt32) -> UInt32
}

class DefaultRandomizer: Randomizer {
    func randomNumber(upperBound: UInt32) -> UInt32 {
        return arc4random_uniform(upperBound)
    }
}

While protocol based dependency injection is great when our API is more complex, when it only has a single purpose (and only requires a single method), we can reduce our complexity by simply using a function instead.

We can see above that DefaultRandomizer is essentially just a wrapper around arc4random_uniform, so why not just simply pass a function of that type when doing dependency injection instead? Like this:

class CardGame {
    typealias Randomizer = (UInt32) -> UInt32

    private let deck: Deck
    private let randomizer: Randomizer

    init(deck: Deck, randomizer: @escaping Randomizer = arc4random_uniform) {
        self.deck = deck
        self.randomizer = randomizer
    }

    func drawRandomCard() -> Card {
        let index = randomizer(deck.count)
        let card = deck[index]
        return card
    }
}

We have now replaced the Randomizer protocol with a simple typealias, and are passing the arc4random_uniform function directly as the default argument for randomizer. We no longer need a default implementation class, and we can still easily mock the randomizer in a test:

class CardGameTests: XCTestCase {
    func testDrawingRandomCard() {
        var randomizationUpperBound: UInt32?

        let deck = Deck(cards: [Card(value: .ace, suite: .spades)])

        let game = CardGame(deck: deck, randomizer: { upperBound in            
            // Capture the upper bound to be able to assert it later
            randomizationUpperBound = upperBound

            // Return a constant value to remove randomness from
            // our test, making it run consistently.
            return 0
        })

        XCTAssertEqual(randomizationUpperBound, 1)
        XCTAssertEqual(game.drawRandomCard(), Card(value: .ace, suite: .spades))
    }
}

I personally really like this technique as it lets me write less code, makes it easier to understand (by bringing the underlying function directly up into the initializer), while still enabling dependency injection.

What do you think?

Feel free to reach out to me on Twitter if you have any questions, suggestions or feedback. I’d also love to hear from you if you have any topic that you’d like me to cover in an upcoming post.

Thanks for reading 🚀

Testing Swift code that uses system singletons in 3 easy steps

Improving Swift compile times