[Swift] Swinjectを使ったDependency Injection
はじめに
こんにちは。
最近生後7ヶ月の娘を見た人から「眉毛が垂れててお父さんそっくりですね〜」と言われるのが定番になっている加藤です。
さて、今回はSwiftのDIフレームワークであるSwinjectの話です。
DIとは
DIはDependency Injectionの略で、日本語では「依存性の注入」と訳されます。
「DIとは何か」を説明した記事は山ほどありますが、ここでは一番端的に表現している記事を見つけたので引用します。
“依存性注入とはインスタンス変数にオブジェクトを与えるということです。本当にただそれだけです。” - James Shore
Swiftにおける現実的なモックより引用。
つまり、あるオブジェクトが依存しているオブジェクトを外から渡してあげることを意味します。
依存しているオブジェクトを中で作らずに外から渡してあげることで、オブジェクト同士を柔軟に組み合わせることが可能になったり、テストしやすくなったりと様々なメリットがあります。
Swinjectとは
SwinjectはSwiftの軽量なDIフレームワークです。MITライセンスで公開されています。
使用するために必要な条件は以下の通りです。
- 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)
検証環境
本記事の検証環境は以下の通りです。
- Xcode Version 8.2.1(8C1002)
- Swift Version 3.0.2
- CocoaPods Version 1.2.0
Swinjectを導入する
Carthage もしくは CocoaPodsでインストール可能です。
今回はCocoaPodsでインストールしました。
Podfileに以下のように記載し、pod install
を実行すればOKです。
source 'https://github.com/CocoaPods/Specs.git' platform :ios, '9.0' target 'ターゲット名' do use_frameworks! pod 'Swinject', '~> 2.0.0' pod 'SwinjectStoryboard', '~> 1.0.0' end
※Swinject
と一緒にSwinjectStoryboardもインストールしていますが、こちらはStoryboardでインスタンス化されたViewControllerに自動的にDIを行うためのSwinjectの拡張です。必須ではありませんが、Storyboardを使って画面を作るなら入れた方が良いかと思います。
Swinjectはバージョン2.0.0、SwinjectStoryboardはバージョン1.0.0がインストールされました。
Swinjectの基本的な使い方
READMEから基本的な使い方を見ていきます。
依存関係を持つモデルを定義する
以下のようにAnimal
プロトコル及び当該プロトコルに準拠したCat
クラスを定義します。
protocol Animal { var name: String? { get } } class Cat: Animal { let name: String? init(name: String?) { self.name = name } }
さらに、Person
プロトコル及び当該プロトコルに準拠したPetOwner
クラスを定義します。
protocol Person { func play() } class PetOwner: Person { let pet: Animal init(pet: Animal) { self.pet = pet } func play() { let name = pet.name ?? "someone" print("I'm playing with \(name).") } }
PetOwner
はコンストラクタでAnimal
を受け取り、保持しています。
つまり、PetOwner
はAnimal
に対して依存していますが、Animal
は具体的な実装を持つclassやstructではなく、プロトコルになっています。ここがポイントです。
このようにDIではプロトコル(抽象)に依存するような作りにします。
Swinjectを使ってDIする
モデルが定義できたので、DIしてみます。
Swinjectを使ったDIは基本的に以下の手順となります。
- Container(一般的にDIコンテナと言われる)を作成する。
- Containerに、Service(何かしらに依存しているオブジェクトの型)と、依存性を注入したServiceを生成する処理を登録する。
- ContainerからServiceを取り出して使用する。
import Swinject // 1. Container(一般的にDIコンテナと言われる)を作成する。 let container = Container() // 2. Containerに、Service(何かしらに依存しているオブジェクトの型)と、依存性を注入したServiceを生成する処理を登録する。 container.register(Animal.self) { _ in Cat(name: "Tama") } container.register(Person.self) { r in PetOwner(pet: r.resolve(Animal.self)!) } // 3. ContainerからServiceを取り出して使用する。 let person = container.resolve(Person.self)! person.play() // I'm playing with Tama.
resolve
の結果はOptional型ですが、ContainerにPerson.self
を登録していて取得できることは明らかであるため、強制アンラップを行なっています。
結果からContainerに登録した依存性が解決されていることがわかります。
Service登録処理について
Serviceは使われる前に事前にContainerに登録しておく必要があります。
登録方法はSwinjectStoryboard
を使う場合と使わない場合で異なります。
説明のために以下のようなUIViewControllerを定義し、アプリ起動時の初期画面として表示する場合を考えます。
class PersonViewController: UIViewController { var person: Person? }
上記のUIViewControllerをContainerに登録する方法について、SwinjectStoryboard
を使う場合と使わない場合それぞれ説明します。
SwinjectStoryboardを使う場合
前提として、Main.storyboard
にアプリ起動時の初期画面としてPersonViewController
が登録されているとします。
SwinjectStoryboardを使う場合は以下のようにSwinjectStoryboardのextensionでsetup()
メソッドを定義し、メソッドの中でServiceをContainerに登録します。
SwinjectStoryboardにはデフォルトのコンテナ(defaultContainer
)が用意されているため、それを使います。
import SwinjectStoryboard extension SwinjectStoryboard { class func setup() { defaultContainer.register(Animal.self) { _ in Cat(name: "Tama") } defaultContainer.register(Person.self) { r in PetOwner(pet: r.resolve(Animal.self)!) } defaultContainer.storyboardInitCompleted(PersonViewController.self) { r, c in c.person = r.resolve(Person.self) } } }
ContainerにPersonViewController
を登録するだけで、SwinjectStoryboardがPersonViewController
の解決を勝手にやってくれます!
便利ですね。
SwinjectStoryboardを使わない場合
Storyboardを使わずにアプリ起動時の初期画面としてPersonViewControllerを表示する場合を考えます。 SwinjectStoryboardを使わない場合は通常、AppDelegateでService登録処理を行います。
let container = Container() { c in c.register(Animal.self) { _ in Cat(name: "Tama") } c.register(Person.self) { r in PetOwner(pet: r.resolve(Animal.self)!) } c.register(PersonViewController.self) { r in let controller = PersonViewController() controller.person = r.resolve(Person.self) return controller } } func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { // UIWindowを生成 let window = UIWindow(frame: UIScreen.main.bounds) window.makeKeyAndVisible() self.window = window // コンテナからUIViewControllerを取得してrootViewControllerに設定 window.rootViewController = container.resolve(PersonViewController.self) return true }
こちらは自分でContainerに登録したUIViewControllerを取得する必要があります。
おわりに
今回はSwinjectを使ってDIを行ってみました。
アプリの規模が大きくなるとモジュール間の依存関係は複雑になりがちです。
依存関係を一元管理し、変更容易性を高めるためにも使えるのではと思います。