今回は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は自身単体で初期化できて、それ以外は一つ上位のモジュールがないと初期化できない様になっています。
実装
いきなりですが、こう書きます笑
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も同じ様に、、、
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
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
を使うために
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~
を足して行くだけで他のクラスの中にごちゃごちゃ書かずに済みますね
終わりに
今回のコード&説明のここがまずい!や、もっといい方法あるよ〜といったアドバイス俄然お待ちしております