iOSのDependency Injectionとは
最近、下記のアプリを作って気づいたことをアウトプット。
ネット上に転がってるiOS開発におけるDIの話は、だいたい以下のような内容だと思う。
- ViewControllerの初期化の時に、必要なプロパティを持つモデルや値をViewControllerへ注入すること
- ViewControllerから見ると、モデルや値に依存していることになる
DIにより何が嬉しいのかというと、以下のような点になる。
- 必須のプロパティを漏らすことなく、安全にViewControllerを生成し利用できる
- 必須のプロパティが明示的になることで、アプリケーション全体の保守性があがる
今なら「なるほど、便利だなー!」と思うのだが、iOS開発しか経験がない僕は当初、Dependency Injection がまったくわからなかった。 僕は今の業務ではStoryboardを使用しない。ブログやLT資料の多くは、Storyboardを前提として内容が構成されている。 なぜDependency Injectionについてたくさん発表が行われているんだろうと、最初は不思議に思っていた。
コードで書くDependency Injection
業務ではStoryboardを使用しないが、個人開発ではStoryboardを使用して開発している。 個人開発は 「業務ではやらないことをやる」 をモットーにしているからだ。 コードでiOSを開発するとき ViewControllerの初期化は下記のように書くと思う。
final class WebViewController: UIViewController, UIWebViewDelegate { private var webView: UIWebView? private let url: URL init(url: URL, title: String) { self.url = url super.init(nibName: nil, bundle: nil) navigationItem.title = title.ex.localized } required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } // 省略
カスタムイニシャライザを定義して、引数をセットするプロパティは private let
で外部からの変更不可にする。
そして、親のイニシャライザを呼ぶ前に子のプロパティをセットすれば、プロパティにデフォルト値を書かずにプロパティを宣言できる。
これで初期化時に何かしらの値が設定されていないと、ビルドエラーなのでurlが初期化のときに設定されることが保証される。
これで「ああ、WebViewControllerはurlとtitleが必要でurlは初期化時に設定されるんだなー」てのが理解できる。 下記のスクショのように書けば、Xcodeが補完して呼び出し側も何が必要なのか理解しやすい。
僕は普段、このような初期化の書き方をDIとは呼んでなく、そういう呼び方があることを知らなかった。ただ 「ViewControllerの初期化の時に、モデルや値を必要なプロパティに設定したい」 と思って書いていた。 今考えれば、ブログやLT資料のDIが目指す内容に近いことをしていると思う。もちろん、Storyboardは使えないけど。
Storyboard で書くDependency Injection
さて、これをStoryboardでやろとするとできない。カスタムイニシャライザが定義できないからだ。
初期化の後にプロパティへ値やモデルを設定しないといけない。つまり、プロパティは internal
や public
や open
にしないといけない。
この場合、下記のデメリットが発生する。
- プロパティの設定忘れ
- プロパティが途中で変更される可能性
- 初期化で必須なのか、それともある状態のもとで必要なのか、プロパティの意図が曖昧になる
だから、 DIが必要なんです という話になる。これで初心者のiOSエンジニアにDIの意図が伝えられる気がする。 Storyboard で書くDependency Injection はだいたい2パターンで、OSSのライブラリを使うか、独自の protocol 等を定義して実装するのどちらか。
いろいろ見て個人的に一番良かったのは、@motokiee さんのDI実装だと思った。
元の資料のリンクは削除されていたけど(@motokieeさんに確認をとって資料のURLを教えてもらった【10/11更新】)、下記のような実装だったと思う。
DependencyInjectable
という プロトコルを定義
protocol DependencyInjectable { associatedtype Dependency static func make(withDependency dependency: Dependency) -> Self }
DependencyInjectable
をViewControllerに継承させる
final class FaqViewController: UITableViewController, DependencyInjectable { private var faq: Faq! // MARK: - DependencyInjectable struct Dependency { let faq: Faq } static func make(withDependency dependency: Dependency) -> FaqViewController { let vc = Storyboard.faq.instantiate(FaqViewController.self) vc.faq = dependency.faq return vc } // 省略
- 呼び出し側
// MARK: - FAQに遷移 private func transitionToFaq(_ faq: Faq) { let dependency = FaqViewController.Dependency(faq: faq) let vc = FaqViewController.make(withDependency: dependency) navigationController?.pushViewController(vc, animated: true) }
このDI実装の良いところは、とにかく他のDIに比べて軽量でわかりやすいところ。 そして、Dependency構造体の表現力がとても豊かでよかった。 今後、プロパティが増えたり、ある時に初期化するときはnilだったり型が違う場合でも表現しやすい。 さらに、下記のようにXcodeで呼ぶときも補完が出て、コードで書いた時と同様に何が必要なのかわかりやすい。
個人開発したBoulderはこのDI実装をフル活用して開発した。
この DependencyInjectable
をさらに用途を限定して型を縛ったりしていたが、正直このままの方が使い勝手は良いと思った。
終わりに
もっと良いDIの実装方法は、必ずあるのでインプットは続けるし教えて欲しい。 ブログとかQiitaではStoryboardを前提とした記事が多いので、コードとStoryboardの両方から説明するものがあると独自性だしていけるかもと思ったり。 両方から説明することで、あるデザインパターンを理解しやすくなることはありえそうなので、頑張って書こうかな。