Swinject+カプセル化でスッキリDI (Swift4)


今回はSwift用有名DIコンテナライブラリであるSwinjectを使ってDIする際に、キレイにコンポーネントを分けて行う方法を共有します

基本

だいたいどこかの紹介サイト(やドキュメント)には、

protocol Animal {
    var name: String? { get }
}

class Cat: Animal {
    let name: String?

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

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).")
    }
}

こげな依存関係があったとして、これをAppDelegateなどで

let container = Container()
container.register(Animal.self) { _ in Cat(name: "Mimi") }
container.register(Person.self) { r in
    PetOwner(pet: r.resolve(Animal.self)!)
}

こうして使う!とあるのですが、、、
実際問題として、毎回DIする際にこういう風に書いていたら、上記くらいの単純さなら大丈夫ですが
もっと複雑&多量な依存関係になってくるとコードが長くなっていって確実に「じゃ、じゃ、、、邪魔やねん!」となります。

今回はAssembler&AssembleというSwinjectに用意されているクラスを使ってスッキリ書く方法を共有します。

依存関係

今回お題とする依存関係の内訳は、、、
ViewController
-> ViewModel
-> Usecase
-> Repository
とこんな感じで、これを一発でスッキリDIすることが目標です。
Repositoryは自身単体で初期化できて、それ以外は一つ上位のモジュールがないと初期化できない様になっています。

実装

いきなりですが、こう書きます笑

UsecaseAssembly.swift
import Swinject

final class UsecaseAssembly: Assembly {
    func assemble(container: Container) {
        registerUsecase(container: container)
        registerRepository(container: container)
    }

    private func registerUsecase(container: Container) {
        container.register(SampleUsecaseProtocol.self) { (_, repository: SampleRepositoryProtocol) in
            SampleUsecase(repository: repository)
        }
    }

    private func registerRepository(container: Container) {
        container.register(SampleRepositoryProtocol.self) { _ in
            SampleRepository()
        }
    }
}

Assemblyクラスにはpublic func assemble(container: Swinject.Container)という関数がprotocolとして定義されおり、これを使うことにより、渡したContainerに依存性を解決する処理を記述したコールバックを登録することができます。
ちなみに、ここでは依存関係を登録するだけで、実際にそれを解決してインスタンスを作るのはまだです。

そしてViewModelも同じ様に、、、

ViewModelAssembly.swift
final class ViewModelAssembly: Assembly {
    func assemble(container: Container) {
        registerViewModel(container: container)
    }

    private func registerViewModel(container: Container) {
        container.register(SampleViewModelProtocol.self) { (_, usecase: SampleUsecaseProtocol) in
            SampleViewModel(usecase: usecase)
        }
    }
}

そしてViewController

ViewControllerAssembly.swift
final class ViewControllerAssembly: Assembly {
    func assemble(container: Container) {
        registerViewController(container: container)
    }

    private func registerViewController(container: Container) {
        container.register(SampleViewController.self) { _ in
            let repository = container.resolve(SampleRepositoryProtocol.self)!
            let usecase = container.resolve(SampleUsecaseProtocol.self, argument: repository)!
            let viewModel = container.resolve(SampleViewModelProtocol.self, argument: usecase)!
            // ViewControllerのインスタンス化の方法は色々あると思うので、それを使ってください。
            let vc = SampleViewController(viewModel: viewModel)
            return vc
        }
    }
}

ここで一気に依存関係を解決 & インスタンスを作成します

これらの用意したAssemblyを使うために

Container.swift
import Swinject

var resolver: Resolver {
    return assembler.resolver
}

fileprivate let assembler = Assembler([ViewControllerAssembly(),
                                       ViewModelAssembly(),
                                       UsecaseAssembly()])

こげなファイルを用意して、Assemblerクラスに用意したAssemblyたちを配列として全部渡してやります。
(resolverはどこからでもアクセスできる様にグローバルで)

ここまでやるとあとは一例ですが、

class AppDelegate: UIResponder, UIApplicationDelegate {
    var window: UIWindow?

    func application(
        _ application: UIApplication,
        didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey : Any]? = nil) -> Bool {

        let window = UIWindow(frame: UIScreen.main.bounds)
        window.makeKeyAndVisible()
        self.window = window

        window.rootViewController = resolver.resolve(SampleViewController.self) // <- ここですよ〜

        return true
    }
}

はい、一行でDI完了しました
この構成だと、例えばもし画面がたくさん増えても既存のAssemblyクラスに
container.register~を足して行くだけで他のクラスの中にごちゃごちゃ書かずに済みますね

終わりに

今回のコード&説明のここがまずい!や、もっといい方法あるよ〜といったアドバイス俄然お待ちしております