シンプルな Swift 関数の依存性注入#
この記事は翻訳です。原文リンク:Simple Swift dependency injection with functions
依存性注入は、コードのデカップリングを実現する優れた手段であり、コードをテストしやすくします。オブジェクトが自分自身の依存関係を作成するのではなく、外部から注入することで、異なるシナリオを設定できるようになります —— 例えば、本番環境 vs テスト環境。
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 を使いやすくするために、randomizer が指定されていない場合にはデフォルト値として 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))
}
}
私はこの技術が特に好きです。なぜなら、より少ないコードを書き、理解しやすく(初期化メソッドに直接関数を置く)、依存性注入を実現できるからです。
あなたはどう思いますか?