Dependency Injection Framework for Swift - Swinjectの紹介

以下のブログ記事の翻訳です[^訳注1]。
[Dependency Injection Framework for Swift - Introduction to Swinject](https://yoichitgy.github.io/post/dependency-injection-framework-for-swift-introduction-to-swinject/)

---

このブログでは、Swift 用の dependency injection (依存性の注入) フレームワークである [Swinject](https://github.com/Swinject/Swinject) を紹介します。Swift 2 では [protocol extension](https://developer.apple.com/library/prerelease/ios/documentation/Swift/Conceptual/Swift_Programming_Language/Protocols.html#//apple_ref/doc/uid/TP40014097-CH25-ID521) が登場し、[protocol oriented programming](https://developer.apple.com/videos/wwdc/2015/?id=408) が推奨されるようになりました。さらに、Xcode 7 では [UI testing](https://developer.apple.com/videos/wwdc/2015/?id=406) ができるようになります。この状況の中で、アプリのコンポーネントをプロトコルによって疎結合にすることがより重要になってきます。疎結合にする方法で代表的なものが dependency injection です。

## Dependency Injection (依存性の注入)

まず具体例で見てみましょう。下のスクリーンショットのように、現在の各地の天気をリスト表示するアプリを開発するとします。APIを利用してサーバから天気の情報を受け取り、そのデータをテーブルビューで表示します。もちろん、ユニットテストは書きますよね。スクリーンショットを見ると、テストではモントリオール[^1]の天気は曇り ("Clouds")、モスクワの天気は快晴 ("Clear")、ロサンゼルスの天気は曇り ("Clouds") となることを確認します。でもちょっと待ってください。もしテストをそのように書くと、明日になってもそのテストはパスするのでしょうか？実際天気は変わるものなので、テストが通ることはまずないですね。

ここでの問題は、ネットワークからデータを取得する部分とそのデータを利用する部分が結合していることです。言い換えると、データを利用する部分がデータを取得する部分に依存しています。もし依存性がハードコードされていると、その依存性の付近でユニットテストを書くことが難しくなります。この問題を解決するには、依存性をどこか別のところから渡してやる必要があります。これが dependency injection (DIと略される) パターンです。外部のコードからクライアントとなるコードに依存性が渡されるのです。依存性を注入する役割を持ったものはDIコンテナ、あるいは単にコンテナと呼ばれます[^2]。

![SwinjectSimpleExampleScreenshot.png](https://qiita-image-store.s3.amazonaws.com/0/86677/9a107f82-122f-5e6e-8e89-96d20495d608.png)

## Swinject

[Swinject](https://github.com/Swinject/Swinject)はSwiftのためにSwiftで書かれた軽量な dependency injection フレームワークです。Swiftのジェネリックスや第一級関数の機能を利用し、このフレームワークのAPIは覚えやすく簡単に使用することができます。Swinjectは[CocoaPods](https://cocoapods.org/)や[Carthage](https://github.com/Carthage/Carthage)でインストールできます。

### CocoaPodsでインストール

CocoaPodsでSwinjectをインストールするには、以下の設定を`Podfile`に書きます[^3]。

    source 'https://github.com/CocoaPods/Specs.git'
    platform :ios, '8.0'
    use_frameworks!

    pod 'Swinject', '~> 0.2.0'

その後`pod install`を実行します。詳しくは[オフィシャルサイト](https://cocoapods.org)をご確認ください。

後で例題のアプリでSwinjectを利用する際は、CocoaPodsでインストールします。

### Carthageでインストール

CarthageでSwinjectをインストールするには、以下の行を`Cartfile`[^4]に追加します。

        github "Swinject/Swinject" ~> 0.2

その後`carthage update`を実行します。詳しくは[プロジェクトのページ](https://github.com/Carthage/Carthage)をご確認ください。

## 基本

例題の天気アプリに戻る前に、Swinjectと dependency injection の基本を見てみましょう。Swinjectのプロジェクトに入っているPlaygroundで簡単に試すこともできます。そのPlaygroundを使用するには、[ソースコード](https://github.com/Swinject/Swinject/releases)をダウンロードするか、[プロジェクト](https://github.com/Swinject/Swinject)をクローンしてください。

### 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を使用すると、依存関係の記述が簡単になり、依存性が注入されたインスタンスを取り出すことも簡単にできるようになります。[次回のブログ記事](http://qiita.com/yoichitgy/items/75b09e6ff06199d7cf80)では、例題の天気アプリを使い、ユニットテストでどのようにSwinjectを使用するか見ていきます。


[^1]: アプリのスクリーンショットに表示されている都市は、1976年以降の夏季オリンピック開催都市です。
[^2]: DI コンテナは他にアセンブラ、プロバイダ、ビルダ、スプリング、インジェクタなどと呼ばれることもあります。
[^3]: Xcode 6.4を使用する場合はバージョン0.1.0を指定してください。バージョン0.2はXcode 7 beta用です。
[^4]: Xcode 6.4を使用する場合はバージョン0.1を指定してください。バージョン0.2はXcode 7 beta用です。
[^訳注1]: 訳注: 英語版の著者本人による翻訳のため、翻訳に関わる著作権上の問題はありません。