GameWith Developer Blog

GameWith のエンジニア、デザイナーが技術について日々発信していきます。

DIKitで人間がクラス間の依存関係を解決するのを終わらせる #GameWith #TechWith

こんにちは、つい最近入社したiOSエンジニアのみなみ(@yuuxeno)です。

DIKitとは、AndroidのDaggerの影響を受けて作られた、iOSアプリ開発(Swift)でも使える、コード生成型のDIライブラリです。コード生成でDIを実現する仕組みは他の言語にも存在し、例えばGo言語にはWireというツールがあります。

先日、potatotips #64において、DIKitについての発表(下のスライド参考)をしました。


今回は、 DIKitとは何なのか?コード生成でDIを実現するメリットとは?などについて、発表した時とは違う話の構成で、ブログ記事で紹介したいと思います。

はじめに

コンポーネント間の依存を解決し、そのコードを書く。ということを、ソフトウェアエンジニアは日常的に何度も行います

例えば、iOSであればViewControllerが単体で使えることは稀です。PresenterやViewModelなど、ViewControllerは大抵なんらかのクラスに依存しています。

// ViewControllerはViewModelに依存
let viewController = ViewController(viewModel: ViewModel())

ViewControllerだけでなく、ViewModel(もしくはPresenter)も大抵なんらかのクラスに依存しています。

// ViewControllerはViewModelに依存、ViewModelはModelに依存
let viewController = ViewController(viewModel: ViewModel(model: Model()))

ViewControllerが依存しているものを"目"で確認します。すると、ViewModelに依存してることが分かりました。今度は、ViewModelが何に依存しているかを確認します。どうやらViewModelはModelに依存しているようです、、、。この間、Xcodeのコード補完の助けを借りながらも、"手"で何度もキーボードをタイプします。この作業をひたすら繰り返します

ViewControllerを生成するというたった1つのことに対して、"人間"が"目"で何度も確認して、"手"でキーボードを何度もタイプしてコードを書く必要があります。

ViewControllerも、ViewModelも、Modelも、1つのアプリにそれぞれいくつもあります。この単純で退屈な作業を、人間が何度も繰り返しています。

人間が依存関係の解決をすることで起こること

DIできない

class ViewController {
    // 生成処理が直接書かれている
    let viewModel = ViewModel(model: Model())
}

ViewController内でViewModelを直接生成しています。ViewControllerに限らず、これは結構あるあるパターンだと思うのですが、何が問題なのでしょうか?

まず、ViewControllerを単体でテストするのが難しくなります。ViewModelをモック用のオブジェクトに差し替えることができないからです。

コードの見通しが悪い

また、ViewControllerにとって重要なのは、ViewModelが使えるかどうか、それだけです。ViewModelがどのように生成されるか(この例ならModelが必要とか)は、ViewControllerにとって知る必要のないことです。

そのクラスがどんな機能持つかと、そのクラスが依存しているオブジェクトの生成方法は、別の種類のものです。全く異なる種類の処理が1つのクラスにあまりに増えることは、コードをぱっと見で把握するのを難しくします。

依存関係を解決する処理を人間が管理する必要

protocol AppResolver {
    func resolveViewController() -> ViewController
}

final class AppResolverImpl: AppResolver {
    // ViewControllerの依存関係を解決する処理
    func resolveViewController() -> ViewController {
        return ViewController(viewModel: ViewModel(model: Model()))
    }
}

let viewController = appResolver.resolveViewController()

今度は、AppResolverという依存関係を管理する専用の場所を作りました。
AppResolver内で対象のクラスに依存性を注入できるようにし、依存関係を解決する処理をAppResolverに分離します。
ViewControllerは単体でテスト可能になりました。依存関係を解決する処理が分離されたことで、ViewControllerのコード自体も少しすっきりしたことでしょう。

これで色々な問題を解決することができました、、、本当にそうなのでしょうか?

依然としてViewControllerの依存関係は、"人間"が"目"で確認し、キーボードを"手"でタイプして解決する必要があります。

なにより、人間が書いたコードは、人間が管理する必要があります。ミスがある可能性があるからです。
例えばメソッド名などが規約通りなのかは、コードレビューで人間がチェックする必要があります(resolveViewControllerでなく、resolveVCになっていないかなど)。
また、引数名や引数自体が変更になった場合など、それぞれのコンポーネントが依存するものに変更があった場合、その修正も人間がする必要があります。

DIKitが可能にすること

DIできなかったり、依存関係を解決する処理が整理されていない、というのは良くあることです。

何故なのでしょうか?DIについての知識がたまたま無かった、かなり急いでいた、、、。理由は色々あるように思えますが、突き詰めれば、依存関係の解決を"人間"がするからではないでしょうか?

人間がするからミスが発生します、全ての人が全てのことに詳しいわけではありません、人によりコードに差が出ます、そしてこれらが色々なところで繰り返されます

解決策はないのでしょうか?

もしかすると、これから紹介するDIKitというライブラリが、1つの選択肢になるかもしれません。全ての問題を解決できるわけではないですが、依存関係の解決を人間がすることの煩わしさを、DIKitにより軽減できます。

DIKitとは?

DIKitとは、コード生成によりDIを実現するためのライブラリです。

DIKitライブラリには、コード生成を行うコマンドラインツールであるdikitgenが含まれています。dikitgenは依存関係を解決 & その処理をコード生成します。これにより、今まで人間が行なっていたクラス間の依存関係の解決を、部分的に自動化することができます。

DIKitは実際に使ってみて真価が分かる系のライブラリです。このライブラリを使うことにより、Xcodeの実行ボタンを押した時(dikitgenが実行された時)に何が起こるのか、ざっくり簡単に説明したいと思います。

ViewController, ViewModel, Modelの3つのクラスがあります。ViewControllerはViewModelに依存、ViewModelはModelに依存しています。
ここで、他のものに依存しているViewController, ViewModelについては、それぞれprotocol Injectableを実装します。
他のものに依存していないModelについては、AppResolver(AppResolverImpl)にその生成処理を追加します。

// ViewController
class ViewController: Injectable {
    struct Dependency {
        let viewModel: ViewModel
    }

    required init(dependency: Dependency) {...}
}

// ViewModel
class ViewModel: Injectable {
    struct Dependency {
        let model: Model
    }

    required init(dependency: Dependency) {...}
}

// Model
protocol AppResolver: Resolver {
    func provideModel() -> Model
}

final class AppResolverImpl: AppResolver {
    func provideModel() -> Model {
        return Model()
    }
}

これで準備は完了です。Xcodeの実行ボタンを押しましょう。

コード生成されるときの様子

生成されたコード

extension AppResolver {

    func resolveModel() -> Model {
        return provideModel()
    }
    
    // ViewControllerの依存関係を解決する処理
    func resolveViewController() -> ViewController {
        let viewModel = resolveViewModel()
        return ViewController(dependency: .init(viewModel: viewModel))
    }

    // ViewModelの依存関係を解決する処理
    func resolveViewModel() -> ViewModel {
        let model = resolveModel()
        return ViewModel(dependency: .init(model: model))
    }

}

今まで手動で書いていた依存関係を解決する処理が、一瞬でコード生成されました。
これぐらいのサンプルコードであれば、手動で依存関係を解決するメリットがあまり感じられないかもしれません。

しかし、そこそこの規模のアプリで、今まで何行も手動で書いていた大量の処理が、一瞬でコード生成され、DIKitの管理下に入る。そんな光景を見ると、このライブラリの持つポテンシャルの高さを感じることができます。(ちなみに後述するのですが、依存関係を解決する全ての処理をコード生成できるわけではないので、その点は注意が必要です)

DIKitでできる

人間の代わりに依存関係を解決

依存関係の解決というのは単純なパズルです。必要なピースは全て決まっていて、あとはひたすらそれをコード全体から集めてくるだけです。その単調な作業をDIKitに任せることができます。

依存関係を解決するコードの管理

それぞれが数行の依存関係を解決する処理でも、プロジェクト全体だとすごい量になります。DIKitはその大量の依存関係を解決する処理を、人間の代わりに管理してくれます。人間が管理する処理を少しでも減らすことができるのは、大きなメリットです。

プロジェクト全体をDIできる仕組みの提供

今はプロジェクトの度に、アプリ全体としてどう依存関係を解決するのかを考える必要があります。依存関係を解決する専用の場所を作るのか、必要なところでそれぞれ依存を解決するのか。依存を解決する処理の規約をどうするのか、、、などです。

依存関係を解決する処理はとても普遍的なものです。星の数ほどあるアプリ、それぞれで独自の仕組みを1から考えるのは効率が良くない気がします。

DIKitは単に依存関係を自動で解決してくれるだけでなく、プロジェクト全体をDIできる仕組みも提供します。DIKitの規約に従いながら、依存関係を解決すれば、あなたのプロジェクトは自然とDIできるものになります

DIKitでできない

全ての依存関係を自動で解決することはできない

DIKitで自動(コード生成)で依存関係を解決できないパターンが何個かあります。

その1つが、protocolの実体クラスが、他のものに依存しているパターンです。
以下のコードでは、ViewControllerがprotocol ViewModelに依存し、そのprotocol ViewModelの実体クラスViewModelImplが、protocol APIClient(実体クラスAPIClientImpl)に依存しています。
ここでは、ViewControllerの依存関係を解決する処理はコード生成されます(resolveViewControllerメソッド)。対して、protocol ViewModelの実体クラスViewModelImplの依存関係を解決する処理は、コード生成自体はされるのですが、そのままでは使えません。この部分だけは、従来通り依存関係の解決を手動で行う必要があるのです。

// APIClient
protocol APIClient {
    ・・・
}

final class APICientImpl: APIClient {
    ・・・
}

// ViewModel
protocol ViewModel {

}

final class ViewModelImpl: ViewModel, Injectable {
    struct Dependency {
        let apiClient: APIClient
    }

    init(dependency: Dependency) {...}
}

// ViewController
final class ViewController: Injectable {
    struct Dependency {
        let viewModel: ViewModel
    }

    init(dependency: Dependency) {...}
}

final class AppResolverImpl: AppResolver {
    func provideAPIClient() -> APIClient {
        return APIClientImpl()
    }

    // ViewModel(ViewModelImpl)の依存関係を解決する処理は、以下のように手動でする必要
    func provideViewModel() -> ViewModel {
        // 一度dikitgenを実行すると、下のコードと同じようなViewModelImplの依存関係を解決する処理が生成されますが、  
    // それを下のコードと置き換えて使ったとしても、いずれにせよdikitgenだけで依存関係を解決することはできません。
        return ViewModelImpl(.init(apiClient: provideAPIClient()))
    }
}

// ViewControllerの依存関係を解決する処理は、以下のようにコード生成される
extension AppResolver {
    func resolveViewController() -> ViewController {
    let viewModel = provideViewModel()
    return ViewController(dependency: .init(viewModel: viewModel))
    }
}

もう1つがシングルトンです。
シングルトンについても、ここら辺のissueを参考に、手動で依存関係を解決する処理を追加する必要があります。

以上のように一部の依存関係については、従来通り人間が解決する必要があります。

ただ、DIKitなどのコード生成型のDIライブラリを使わない従来の方法では、人間が100%依存関係を手動で解決していたことを考えると、一部分でも自動化できるのは大きな前進です。

現状のGameWithアプリにおけるDI

GameWithアプリは現在手動でDIする処理を書いていたり、そもそもDIせず直接生成するコードが書かれていたりバラバラです。
将来的にはプロジェクト全体をDIできるようにするついでに、DIKitなどコード生成型のDIライブラリを入れたいと思っています!

DIKitが持つ大きな可能性

星の数ほどあるアプリ、その全ての依存関係を解決する処理が整理されて、DIもできるものだったら、、、
依存関係を解決する処理を整理したり、DIできるようにリファクタリングすることは不要になり、多くのコストが浮きます。見通しの良いコードがより効率的な開発を可能にします。DIについて考える時間は必要ありません、全ての仕組みはDIKitが用意してくれています

その結果、もっと別のこと、例えばアプリをもっと使いやすいものにする、などのことにリソースを注力できるようになります

そうなれば、もっともっとおもしろいアプリが出てくる、そんな未来がやってくる、そうあなたも思いませんか?

参考

最後に

GameWithのDeveloper向けTwitterアカウントを開設しました。

もくもく会の告知やブログの更新情報などを発信するので良かったらフォロー宜しくお願いします!

twitter.com