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, we can inject them from the outside, allowing 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, you can see that we inject a Randomizer
in the initialization of CardGame
, which is used to generate a random index when drawing. To make the API easy to use, we also assign a default value to it when not given - DefaultRandomizer
. 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 the API we design is very complex, using protocols to implement dependency injection is very good. However, when we only have a simple purpose (just need a simple method), using functions to implement it 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 test the randomizer
:
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 love 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?