今是昨非

今是昨非

日出江花红胜火,春来江水绿如蓝

Simple Dependency Injection in Swift Functions

Simple Swift function dependency injection#

This article is a translation, original link: Simple Swift dependency injection with functions

Dependency injection is a great way to decouple code and make it easier to test. Instead of objects creating their own dependencies, injecting them from the outside allows us to set up different scenarios - for example, in production vs. in testing.

In Swift, most of the time, we use protocols to implement dependency injection. For example, let's say we're writing a simple card game that draws a random card using a Randomizer, as shown below:

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 example above, we can see that we inject a Randomizer into the initialization of CardGame, which is used to generate a random index when drawing. To make the API easier to use, we also assign a default value - DefaultRandomizer - when no randomizer is given. The protocol and default implementation are as follows:

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

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

When designing APIs that are very complex, using protocols to implement dependency injection is very good. However, when the purpose is simple (only requiring a simple method), using functions can reduce complexity.

The DefaultRandomizer above is essentially a wrapper for arc4random_uniform, so why not try implementing dependency injection by passing a function type, as shown below:

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 changed Randomizer from a protocol to a simple typealias, and directly used the arc4random_uniform function as the default parameter for randomizer. We no longer need a default implementation class, and we can easily mock the randomizer for testing:

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 out test, making it run consistently
            return 0
        })

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

I personally really like this technique because it allows us to write less code, is easy to understand (just put the function in the initialization method), and still achieves dependency injection.

What do you think?

Loading...
Ownership of this post data is guaranteed by blockchain and smart contracts to the creator alone.