LoginSignup
149
148

More than 5 years have passed since last update.

Dependency Injection Framework for Swift - Swinjectの紹介

Last updated at Posted at 2015-08-11

以下のブログ記事の翻訳です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

SwinjectSimpleExampleScreenshot.png

Swinject

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

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をインストールするには、以下の行をCartfile5に追加します。

    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に登録した型でpetAnimalTypeが決定されるよう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を使用するか見ていきます。


  1. 訳注: 英語版の著者本人による翻訳のため、翻訳に関わる著作権上の問題はありません。 

  2. アプリのスクリーンショットに表示されている都市は、1976年以降の夏季オリンピック開催都市です。 

  3. DI コンテナは他にアセンブラ、プロバイダ、ビルダ、スプリング、インジェクタなどと呼ばれることもあります。 

  4. Xcode 6.4を使用する場合はバージョン0.1.0を指定してください。バージョン0.2はXcode 7 beta用です。 

  5. Xcode 6.4を使用する場合はバージョン0.1を指定してください。バージョン0.2はXcode 7 beta用です。 

149
148
3

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
149
148