簡単な Swift 関数の依存性注入#
この記事は翻訳です。元の記事のリンク:Simple Swift dependency injection with functions
依存性注入は、コードのカップリングを解消し、テストが容易になる手法です。オブジェクトが自身の依存関係を作成するのではなく、外部から注入することで、異なるシナリオを設定できます - 例えば、本番環境とテスト環境など。
Swift では、ほとんどの場合、プロトコルを使用して依存性注入を実現します。例えば、シンプルなカードゲームを作成し、Randomizer(ランダム生成器)を使用してランダムなカードを描画する場合、以下のようになります:
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
}
}
上記の例では、CardGame の初期化時に、ランダマイザー(Randomizer)が注入されていることがわかります。これは、描画時にランダムなインデックスを生成するために使用されます。API の使用を容易にするために、ランダマイザーが指定されていない場合には、デフォルトの値である DefaultRandomizer が使用されます。以下に、プロトコルとデフォルトの実装を示します:
protocol Randomizer {
func randomNumber(upperBound: UInt32) -> UInt32
}
class DefaultRandomizer: Randomizer {
func randomNumber(upperBound: UInt32) -> UInt32 {
return arc4random_uniform(upperBound)
}
}
API が非常に複雑な場合、プロトコルを使用して依存性注入を実現するのは非常に便利です。ただし、単純な目的(単純なメソッドのみが必要な場合)には、関数を使用することで複雑さを減らすことができます。
上記の DefaultRandomizer は、本質的には arc4random_uniform のラッパーです。そのため、依存性注入を実現するために関数型を渡すことを試してみるのはいかがでしょうか。以下に示します:
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
}
}
Randomizer をプロトコルから単純な typealias に変更し、arc4random_uniform 関数を randomizer のデフォルトパラメータとして直接使用しています。デフォルトの実装クラスは不要であり、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
// 上限値をキャプチャして後でアサートできるようにする
randomizationUpperBound = upperBound
// テストからランダム性を排除するため、一定の値を返す
return 0
})
XCTAssertEqual(randomizationUpperBound, 1)
XCTAssertEqual(game.drawRandomCard(), Card(value: .ace, suite: .spades))
}
}
私はこのテクニックが特に好きです。なぜなら、コードを少なく書くことができ、理解しやすいからです(初期化メソッドに関数を直接配置するだけです)。また、依存性注入も実現できます。
あなたはどう思いますか?