以下のブログ記事の翻訳です1。
Dependency Injection Framework for Swift - Introduction to Swinject
このブログでは、Swift 用の dependency injection (依存性の注入) フレームワークである Swinject を紹介します。Swift 2 では protocol extension が登場し、protocol oriented programming が推奨されるようになりました。さらに、Xcode 7 では UI testing ができるようになります。この状況の中で、アプリのコンポーネントをプロトコルによって疎結合にすることがより重要になってきます。疎結合にする方法で代表的なものが dependency injection です。
Dependency Injection (依存性の注入)
まず具体例で見てみましょう。下のスクリーンショットのように、現在の各地の天気をリスト表示するアプリを開発するとします。APIを利用してサーバから天気の情報を受け取り、そのデータをテーブルビューで表示します。もちろん、ユニットテストは書きますよね。スクリーンショットを見ると、テストではモントリオール2の天気は曇り ("Clouds")、モスクワの天気は快晴 ("Clear")、ロサンゼルスの天気は曇り ("Clouds") となることを確認します。でもちょっと待ってください。もしテストをそのように書くと、明日になってもそのテストはパスするのでしょうか?実際天気は変わるものなので、テストが通ることはまずないですね。
ここでの問題は、ネットワークからデータを取得する部分とそのデータを利用する部分が結合していることです。言い換えると、データを利用する部分がデータを取得する部分に依存しています。もし依存性がハードコードされていると、その依存性の付近でユニットテストを書くことが難しくなります。この問題を解決するには、依存性をどこか別のところから渡してやる必要があります。これが dependency injection (DIと略される) パターンです。外部のコードからクライアントとなるコードに依存性が渡されるのです。依存性を注入する役割を持ったものはDIコンテナ、あるいは単にコンテナと呼ばれます3。
Swinject
SwinjectはSwiftのためにSwiftで書かれた軽量な dependency injection フレームワークです。Swiftのジェネリックスや第一級関数の機能を利用し、このフレームワークのAPIは覚えやすく簡単に使用することができます。SwinjectはCocoaPodsやCarthageでインストールできます。
CocoaPodsでインストール
CocoaPodsでSwinjectをインストールするには、以下の設定をPodfile
に書きます4。
source 'https://github.com/CocoaPods/Specs.git'
platform :ios, '8.0'
use_frameworks!
pod 'Swinject', '~> 0.2.0'
その後pod install
を実行します。詳しくはオフィシャルサイトをご確認ください。
後で例題のアプリでSwinjectを利用する際は、CocoaPodsでインストールします。
Carthageでインストール
CarthageでSwinjectをインストールするには、以下の行をCartfile
5に追加します。
github "Swinject/Swinject" ~> 0.2
その後carthage update
を実行します。詳しくはプロジェクトのページをご確認ください。
基本
例題の天気アプリに戻る前に、Swinjectと dependency injection の基本を見てみましょう。Swinjectのプロジェクトに入っているPlaygroundで簡単に試すこともできます。そのPlaygroundを使用するには、ソースコードをダウンロードするか、プロジェクトをクローンしてください。
Dependency Injection なしの場合
では、動物と遊ぶゲームを作ると考えてみましょう。まず、dependency injection なしでプログラムを書いてみることとします。動物を表すクラスとしてCat
を作ります。
class Cat {
let name: String
init(name: String) {
self.name = name
}
func sound() -> String {
return "Meow!"
}
}
PetOwner
クラスはCat
のインスタンスを保持し、ペットと遊ぶことができるようにします。
class PetOwner {
let pet = Cat(name: "Mimi")
func play() -> String {
return "I'm playing with \(pet.name). \(pet.sound())"
}
}
これでPetOwner
をインスタンス化して遊んでみることができます。
let petOwner = PetOwner()
print(petOwner.play()) // prints "I'm playing with Mimi. Meow!"
もし世の中の全員が猫派であればこれで完璧なのですが、実際は犬派の人もいます。Cat
のインスタンス化がハードコードされているため、PetOwner
クラスはCat
クラスに依存してしまっています。Dog
や他のクラスをサポートするには、この依存性を排除する必要があります。
Dependency Injection ありの場合
Dependency injection を有効利用するのは今です。ここでAnimalType
プロトコルを導入し、依存性から解き放ちましょう。
protocol AnimalType {
var name: String { get }
func sound() -> String
}
このプロトコルに準拠するようにCat
クラスを書き換えます。
class Cat: AnimalType {
let name: String
init(name: String) {
self.name = name
}
func sound() -> String {
return "Meow!"
}
}
さらに、イニシャライザでAnimalType
を注入できるようにPetOwner
クラスを書き換えます。
class PetOwner {
let pet: AnimalType
init(pet: AnimalType) {
self.pet = pet
}
func play() -> String {
return "I'm playing with \(pet.name). \(pet.sound())"
}
}
これでPetOwner
のインスタンスを生成するときに、AnimalType
への依存性を注入することができます。
let catOwner = PetOwner(pet: Cat(name: "Mimi"))
print(catOwner.play()) // prints "I'm playing with Mimi. Meow!"
もし以下のようなDog
クラスがあったとしたら、
class Dog: AnimalType {
let name: String
init(name: String) {
self.name = name
}
func sound() -> String {
return "Bow wow!"
}
}
このように犬とも遊ぶことができますね。
let dogOwner = PetOwner(pet: Dog(name: "Hachi"))
print(dogOwner.play()) // prints "I'm playing with Hachi. Bow wow!"
これまでのところ、PetOwner
の依存性を手で書いて注入してきましたが、もしアプリの開発が進み依存関係が増えてくると、自分たちで dependency injection を管理するのは大変になってきます。それではここで、Swinjectで依存性を管理してみましょう。
Swinjectを使用するには、まず以下の行をPlaygroundやソースコードに追加します。
import Swinject
それからContainer
のインスタンスを作り、依存関係を登録します。
let container = Container()
container.register(AnimalType.self) { _ in Cat(name: "Mimi") }
container.register(PetOwner.self) { r in
PetOwner(pet: r.resolve(AnimalType.self)!)
}
ここでは、AnimalType
が "Mimi" と名付けられたCat
のインスタンスになるようにcontainer
に登録し、さらにcontainer
に登録した型でpet
のAnimalType
が決定されるようPetOwner
を登録しています。container
に登録されていない型をresolve
メソッドで取り出そうとするとnil
を返しますが、AnimalType
は登録済みであることをプログラマの私達が知っているので、!
で強制的にアンラップしています。
これでコンテナの設定が済んだので、container
からPetOwner
のインスタンスを受け取ってみましょう。
let petOwner = container.resolve(PetOwner.self)!
print(petOwner.play()) // prints "I'm playing with Mimi. Meow!"
こんなに簡単に依存関係をContainer
に登録でき、依存関係の解決されたインスタンスを取り出すことができるようになりました。
まとめ
このブログ記事では、天気アプリのユニットテストを書くシナリオで dependency injection のコンセプトを説明した後、簡単なユースケースで試してみました。Swinjectを使用すると、依存関係の記述が簡単になり、依存性が注入されたインスタンスを取り出すことも簡単にできるようになります。次回のブログ記事では、例題の天気アプリを使い、ユニットテストでどのようにSwinjectを使用するか見ていきます。