DIとは
Dependency Injectionの略。日本語で「依存性の注入」
コンポーネント間の依存関係を外部から注入するデザインパターン(と言う一文は理解できなくてOK)
DIしたいアプリの例
UrlProvider
は、通信先URLを生成するクラス
3つのクラスがUrlProvider
を使っている
-
WebView
URLをアプリ内部で表示する -
OpenBrowser
URLをアプリ外部のブラウザで表示する -
HttpClient
URLとバックグラウンドで通信する
実装はこんな感じ(関係ない部分は省略)
class WebView {
private val urlProvider = UrlProvider()
・・・
}
class OpenBrowser {
private val urlProvider = UrlProvider()
・・・
}
class HttpClient {
private val urlProvider = UrlProvider()
・・・
}
懸念
UrlProvider
を別々に初期化するので、
- メモリを2倍、3倍食う
- どれかが変更されたら、他2箇所と内容が変わる
- UrlProviderが完成するまで、動作確認もテストも出来ない
改善すると、こうなる
class WebView(urlProvider: UrlProvider) { ・・・ }
class OpenBrowser(urlProvider: UrlProvider) { ・・・ }
class HttpClient(urlProvider: UrlProvider) { ・・・ }
class MainActivity {
private val urlProvider = UrlProvider()
// 完成までは以下を使う
// private val urlProvider = MockUrlProvider()
override fun onCreate(savedInstanceState: Bundle?) {
val webView = WebView(urlProvider)
val openBrowser = OpenBrowser(urlProvider)
val httpClient = HttpClient(urlProvider)
・・・
}
}
先述の一文に則れば、一応この時点でDIできたと言える
[NOTE]オブジェクト生成時に外部から必要なものを注入しているから、DIの条件を満たす
でも、アプリの開発が進んでこうなったら?
悪夢!!
その時の実装はこんな感じ(めっちゃ省略)
class LoginFragment(urlProvider: UrlProvider) { ・・・ }
class WebViewFragment(urlProvider: UrlProvider) { ・・・ }
class HomeFragment(urlProvider: UrlProvider) { ・・・ }
class LoginViewModel(urlProvider: UrlProvider) { ・・・ }
class FaqViewModel(urlProvider: UrlProvider) { ・・・ }
class BannerViewModel(urlProvider: UrlProvider) { ・・・ }
class Login(urlProvider: UrlProvider) { ・・・ }
class LoginRepository(urlProvider: UrlProvider) { ・・・ }
class LoginService(urlProvider: UrlProvider) { ・・・ }
ダサいのは一目瞭然だが、具体的にどうダメなのか
UrlProvider
の引数を変えたり、UrlProvider
自体を別のものに変えた時、ほぼ全部のクラスを直さなきゃならなくなる
そこでDIツール登場!
あっ、UrlProvider
は全クラスに渡さなくてもいいね!(うっかり)
これが最小限のDI実装
DIツールの役割(重要!!)
- インスタンスを1個作る(UrlProvider)
- 作ったインスタンスを必要としているクラスに配布する(To: FaqViewModel, BannerViewModel, LoginService)
- 引数を使わず初期化できるようサポートする(To: HomeFragment, WebViewFragment, LoginRepository)
ちなみに1は、1個と言わず複数作ることも可能(Singletonパターン/Factoryパターンを選べる)
DIツール適用手順
1. DIツールの設定(前頁の役割1,2)
Koinの例
class KoinApp : Application() {
override fun onCreate() {
super.onCreate()
startKoin {
modules(module)
}
}
val module = module {
single { UrlProvider() } // インスタンス作成
single { LoginService(get()) } // 1つ目のクラスにインスタンス注入
viewModel { FaqViewModel(get()) } // 2つ目のクラスにインスタンス注入
viewModel { BannerViewModel(get()) } // 3つ目のクラスにインスタンス注入
}
}
ちなみにインターフェースと実体がある場合はこう書く
single { UrlProviderImpl() as UrlProvider }
2. 各クラスでの初期化処理(前々頁の役割3)
class LoginRepository() {
private val loginService: LoginService by inject() // DIツールからインスタンスを受け取る
・・・
}
class WebViewFragment : Fragment() {
private val faqViewModel: FaqViewModel by viewModel() // DIツールからインスタンスを受け取る
・・・
}
class HomeFragment : Fragment() {
private val bannerViewModel: BannerViewModel by viewModel() // DIツールからインスタンスを受け取る
・・・
}
このように、DIツールのサポートを受けて初期化する
以上!
DI初心者が流し読みしても理解できそうなテンポで書いたつもりですが、おわかりいただけたでしょうか。
夜中に眠れず勢いで書いた記事なので、ここがわかりづらい!とか間違ってる!とかあればコメントください。
最後にDIのメリット/デメリット
- メリット
- クラス同士の依存が減り、クラスの差し替えが容易になる
- インスタンスをあちこちで使いまわせる
- 通信処理をダミーに差し替えるなどすれば、単体テストが容易になる
- クリーンアーキテクチャーを構築できる
- デメリット
- 最初から全体を考えて複数クラスを同時実装するので、初期開発コストがかかる
- 学習コストがかかる
- 処理速度が多少伸びる
- DIツール自体を差し替えるのが難しい
最後に
実際にDIを使ったサンプルアプリUnusedAppFinderもあるので、半年間メンテを怠っていますが参考にして下さい。
余談:DIを適用した車!?