Closed2

【Swift】DIKit 0->1

yoshitakayoshitaka

コード生成による静的なDependencyInjection

DI = 必要なものを外から渡す

DIなし:密結合だがインスタンスの生成は簡単

DIあり:疎結合だがインスタンスの生成が面倒

DIをサポートする仕組み

自動的にインスタンスを生成できる型を方法とセットで登録しておく

  • DI用のinitializerを登録する
  • DI用のprovider methodを用意する

DI用のinitializer

protocol Injectable {
    associatedtype Dependency
    init(dependency: Dependency)
}

final class APIClient: Injectable {
    struct Dependency {
        let urlSession: URLSession
        let cache: Cache
    }
    init(dependency: Dependency) { ... }
}

DI用のprovider method

protocol Resolver { } 
protocol AppResolver: Resolver {
    func providerURLSession() -> URLSession
    func providerCache() -> URLSession
    func provideAPIClient(urlSession: URLSession, cache: Cache) -> APIClient
}

依存関係を解決するコード生成

記述されているコードを解釈して、それを補完するコードを生成
Resolverをマークとして使用。
(Sourcekitten というライブラリを使用してコードを解釈している。)

struct A: Injectable {
    struct Dependency {
        let b: B
    }
    init(dependency: Dependency) { }
}

struct B { }

protocol ABResolver: Resolver {
    func provideB() -> B
}

AはBに依存していて、BはABResolverのメソッドによって提供されている

これで自動生成されるのがresolveAとresolveBというそれぞれAとBのインスタンスを返すメソッド

extension ABResolver {
    func resolveA() -> A {
        let b = resolveB()
        return A(dependency: .init(b: b))
    }
    func resolveB() -> B {
        return provideB()
    }
}

ここで重要なのはresolveAの中で先にBを生成していて、その生成したBを使ってAを生成していること

final class ABResolverImpl: ABResolver {
    let b: B = ...
    func provideB() {
        return b
    }
}

これまでにprovideB()はプロトコルで宣言されていただけなので、ここで初めて実装している。そしてコレで好きなBを返せるようになった。


ちょっと余談、なぜプロトコルにしたの?

プロバイダーメソッドの実装なしでグラフが組めること

これは多分最後に実装したやつのことだと。。。
どうやら提供されるインスタンスの差し替えができるのでテストで便利だったりするみたい。

利用時に必要なものが揃っているということ

必要なものをプロトコルで全て宣言したので、すべて揃わないとコンパイルできない


final class UserProfileViewController: UIViewController, Injectable {
    struct Dependency {
        let userID: Int64
        let apiClient: APIClient
    }
    init(dependency: Dependency) { }
}
final class APIClient { ... }

protocol AppResolver: Resolver {
    func provideAPIClient() -> APIClient
}

インジェクタブルでもプロバイダーメソッドも提供されていないuserID:Int64についてもコード生成ができる

extension AppResolver {
    func resolveAPIClient() -> APIClient {
        return provideAPIClient()
    }
    func resolveUserProfileViewController(userID: Int64) -> UserProfileViewController {
        let apiClient = resoveAPIClient()
        return UserProfileViewController(dependency: .init(userID: userID, apiClient: apiClient)
    }
}

プロバイダーメソッドの引数として都度値を指定できるようになった。

このスクラップは2021/03/17にクローズされました