Takahiro Octopress Blog

-1から始める情弱プログラミング

SwinjectでDIを意識してみよう!

はじめに

筆者はここ1年間、主にバックエンド構築にSpring Bootを利用してきました。
Spring Framework自体は2002年頃にリリースされたそうなのですが、Spring BootはSpringベースでのアプリ開発を楽にしてくれる新たな形で様々なSpringのFramework群を統合したものとのことです。
(Spring Bootでググると2〜3年前辺りからの記事が多い印象です。)

Springの特徴は何と言っても DI でしょう。
今回はそのSpringの代名詞とも言える DI をSwiftで扱ってみたいと思います。

DIとは

DIとは『Dependency Injection』の略語で、日本語だと依存性の注入と翻訳されたりします。
具体的に何者なのかと言うと、

  • コンポーネント間の依存関係を排除するソフトウェアパターン (Wikipedia – 依存性の注入)
  • これにより疎結合性が高まるので単体テストが書きやすい

というものです。

Springベースのアプリでは基本パターンとして利用しています。
詳しい説明は省きますが、
Spring起動時にBean化したオブジェクトを@Autowiredアノテーションを利用することで、DIコンテナ経由で簡単に DI を利用することができます。

注意しておきたいこととして、
Bean定義したクラスは@Autowiredアノテーションで呼び出される際、デフォルトでシングルトンパターンとして生成されたオブジェクトを呼び出しています。

SwiftでDIするには?

さて、そんなSpringの特徴的なDIですが、Swiftアプリで利用することはできるのでしょうか?
筆者が調べたところ、最も人気のあるSwift版DIライブラリとしてSwinjectというものがあるようです。
本日時点でGitHub上のスター数が1,432となっており、なかなかの注目度かと思います。

しかも、下記のようにSwift3にも対応しているのが嬉しいですね!

1
2
3
4
5
6
7
iOS 8.0+ / Mac OS X 10.10+ / watchOS 2.0+ / tvOS 9.0+
Swift 2.2 or 2.3
Xcode 7.0+
Swift 3
Xcode 8.0+
Carthage 0.18+ (if you use)
CocoaPods 1.1.1+ (if you use)

実際にSwinjectを利用してSwiftでのDIを試してみたいと思います。

Swinjectの使い方

では早速使ってみましょう。

Swinjectのインストール

CocoaPodsでインストールするために下記のようにPodfileを作成しましょう。

1
2
3
4
5
6
7
8
9
10
// Podfile
source 'https://github.com/CocoaPods/Specs.git'
platform :ios, '8.0' # or platform :osx, '10.10' if your target is OS X.

target 'SwinjectSample' do
  use_frameworks!

  pod 'Swinject', '~> 2.0.0'
  pod 'SwinjectStoryboard', '1.0.0'
end

その後にpod installを実行しましょう。
これでSwinjectの用意は完了です。

DIしたいクラスの定義

サンプルとしてDIしたいクラスを定義します。
具体的なクラスだけでなく プロトコル を定義しているのは、
後々、同じようなJavaProgrammerクラスを作成したくなった際に便利だからです。
(ベースが同じで、拡張機能が必要になった際に、0から書き直す必要がなくなります。)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// Engineer.swift
import Foundation

protocol Engineer {
    var name: String? { get set }
    func develop() -> String
}

class SwiftProgrammer: Engineer {
    var name: String?

    init(name: String?) {
        self.name = name
    }

    func develop() -> String {
        return "Let's start developing a swift application!"
    }
}

続いて、先程作成したEngineerを参照するCompanyOwnerクラスを作成します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// Company.swift
import Foundation

protocol Person {
    func hire() -> String
}

class CompanyOwner: Person {
    let humanResource: Engineer

    init(humanResource: Engineer) {
        self.humanResource = humanResource
    }

    func hire() -> String {
        let name = humanResource.name ?? "someone"
        return "I'm hiring \(name). \(humanResource.develop())"
    }
}

ここでSwiftProgrammerクラスでなくEngineerプロトコルを参照することで、CompanyOwnerクラス自体がSwiftProgrammerクラスに依存することがなくなりました。
つまりJavaProgrammerを雇いたいCompanyOwnerクラスが必要になった場合、作成するのはJavaProgrammerクラスのみで良くなります。
これはこれでCompanyOwnerクラスに対して単体テストコードを書く時にSwiftProgrammerクラスに依存せずに書くことができます。

DIコンテナへのクラスの登録

さて、DIしたいクラスが定義できたので、そのクラスをDIコンテナに登録します。
公式ページによるとSwinjectStoryboardを利用する場合と利用しない場合の2種類の方法があるそうですが、今回は簡単に対応可能なSwinjectStoryboard利用する方法で書いてみます。

サンプルとして、ViewController.swiftでDIコンテナ経由で呼び出したクラスを利用したいとします。
その場合、下記のように、ViewController.swiftにコンテナ定義を書きます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// ViewController.swift
import UIKit
import SwinjectStoryboard

extension SwinjectStoryboard {
    class func setup() {
        // (1)
        defaultContainer.register(Engineer.self) { _ in SwiftProgrammer(name: "Takahiro") }
        // (2)
        defaultContainer.register(Person.self) { r in
            CompanyOwner(humanResource: r.resolve(Engineer.self)!)
        }
        // (3)
        defaultContainer.storyboardInitCompleted(ViewController.self) { r, c in
            c.person = r.resolve(Person.self)
        }
    }
}
...

順々に説明すると、

  1. defaultContainerEngineer指定でSwiftProgrammer(name: "Takahiro")が呼び出されるように登録
  2. defaultContainerPerson指定でCompanyOwner(humanResource: r.resolve(Engineer.self)!)が呼び出されるように登録
    ※ 1でEngineer指定でSwiftProgrammer(name: "Takahiro")呼び出しをセットしているので、それが2のCompanyOwnerの引数にセットされます。
  3. defaultContainerにDIコンテナ経由での呼び出し先と先ほどまで定義していたPerson指定での呼び出し元を紐付け

となっています。
これにより、下記のようにViewController.swift内でDIコンテナ経由でPerson指定で想定した処理を呼び出すことができます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// ViewController.swift

class ViewController: UIViewController {
    var person: Person?

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.
        print(person?.hire() ?? "I can't hire engineers.")
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }

}

上記を見ると、ViewControllerクラスの中でPersonをインスタンス化している箇所はないことがわかると思います。
つまり、DIコンテナ経由でPersonを呼び出せているわけですね。

まとめ

さて、Swinjectを利用してサンプルを書いてみた感想ですが…
正直、Spring Bootと比較すると、Swiftでは手動で書くべきところが多いと感じました。
また、様々なUIViewControllerにまたがって利用する場合は記載箇所に一工夫必要なのかなとも思いました。
(毎回、全UIViewController系のファイルにsetUp()を書くわけにもいかないと思いますし…)

まだまだ未知数なところがあるので、継続して試してみたいところではあります。
と言ったところで本日はここまで。

参考:

Comments