DI (Dependency Injection) とは何なのか?
そして、なぜ便利なのかを簡単に説明しようと思います。
他のクラスに依存していない場合
まずは何者にも依存していない簡単なViewModel
を作ってみます。
ViewModel
class GreetingViewModel {
func greet() -> String {
return "Hello"
}
}
class GreetingViewModel {
fun greet(): String {
return "Hello"
}
}
greet
メソッドが返す文字列を出力するコードはとりあえずこんな感じ。
var viewModel = GreetingViewModel()
print(viewModel.greet())
val viewModel = GreetingViewModel()
println(viewModel.greet())
こいつらを何かしらの方法(SwiftPlaygroundやTry Kotlin等)で動かしてみると...
結果
Hello
という感じでHello
と出力されます。
当たり前ですね。
他のクラスに依存している場合
greet
メソッドが返す文字列を、他のクラスに依存するようにしてみます。
依存するクラス
class GreetingRepository {
func greet() -> String {
return "Hello from Repository"
}
}
class GreetingRepository {
fun greet(): String {
return "Hello from Repository"
}
}
ViewModel
がこのクラスに依存するようにします。
ViewModel
class GreetingViewModel {
private var greetingRepository = GreetingRepository()
func greet() -> String {
return self.greetingRepository.greet()
}
}
class GreetingViewModel {
private val greetingRepository = GreetingRepository()
fun greet(): String {
return this.greetingRepository.greet()
}
}
結果
Hello from Repository
GreetingRepository
クラスから返ってきた文字列が表示されました。
問題なしですね。
依存するクラスを外から与える
では、このGreetingViewModel
クラスの中で生成しているGreetingRepository
のインスタンスを
クラスの外で生成してGreetingViewModel
クラスに与えてみます。
ViewModel
ViewModel
クラスのコンストラクタ引数でGreetingRepository
のインスタンスを受け取るように修正します。
class GreetingViewModel {
private var greetingRepository: GreetingRepository
init(greetingRepository: GreetingRepository) {
self.greetingRepository = greetingRepository
}
func greet() -> String {
return self.greetingRepository.greet()
}
}
class GreetingViewModel(private val greetingRepository: GreetingRepository) {
fun greet(): String {
return this.greetingRepository.greet()
}
}
そうすると出力用コードはこんな感じになるかと思います。
var viewModel = GreetingViewModel(greetingRepository: GreetingRepository())
print(viewModel.greet())
val viewModel = GreetingViewModel(greetingRepository = GreetingRepository())
println(viewModel.greet())
これを実行してみると...
結果
Hello from Repository
先ほどと同じ出力になり、外から依存クラスのインスタンスを与えた場合でも同じ挙動になりました。
これを Dependency Injection (依存性の注入) と呼びます。
ただ、この状態ではクラス内でインスタンス化するのとほとんど変わりません。
なにか利点はあるのでしょうか?
たとえば、注入するGreetingRepository
がまだ完成していないとしたらどうでしょう?
インターフェイス(プロトコル)の活用
まだ完成していないクラスを使いたいとなったら、モックアップになるクラスを作って本物の代わりをさせます。
こんな感じでしょうか。
モックアップ
class GreetingRepositoryMock {
func greet() -> String {
return "Hello from Mock"
}
}
class GreetingRepositoryMock {
fun greet(): String {
return "Hello from Mock"
}
}
では出力用コードをこんな風に書き換えて...
var viewModel = GreetingViewModel(greetingRepository: GreetingRepositoryMock())
print(viewModel.greet())
val viewModel = GreetingViewModel(greetingRepository = GreetingRepositoryMock())
println(viewModel.greet())
あれっ!?
「型が違います」みたいなこと言われてビルドできない!!
そうなんです。
GreetingRepository
とGreetingRepositoryMock
では型が違うので引数として渡せないんです。
かと言って、引数の型をGreetingRepositoryMock
にしてしまうと、クラス完成後にまた修正しなくてはなりません。
そこでインターフェイス(プロトコル)を使います。
インターフェイス(プロトコル)というのは、クラスが実装すべきメソッドやプロパティを定義する型枠です。
インターフェイス(プロトコル)
クラスにはgreet
というメソッドがあれば良いので、こんな感じになります。
protocol GreetingRepositoryProtocol {
func greet() -> String
}
interface GreetingRepositoryInterface {
fun greet(): String
}
ViewModel
引数の型を、クラスではなくインターフェイス(プロトコル)に変更します。
こうしておくと、インターフェイス(プロトコル)を実装したクラスであれば何でも渡せるようになります。
class GreetingViewModel {
private var greetingRepository: GreetingRepositoryProtocol
init(greetingRepository: GreetingRepositoryProtocol) {
self.greetingRepository = greetingRepository
}
func greet() -> String {
return self.greetingRepository.greet()
}
}
class GreetingViewModel(private val greetingRepository: GreetingRepositoryInterface) {
fun greet(): String {
return this.greetingRepository.greet()
}
}
GreetingRepositoryクラス
インターフェイス(プロトコル)を実装したクラスになるように修正します。
class GreetingRepository : GreetingRepositoryProtocol {
func greet() -> String {
return "Hello from Repository"
}
}
class GreetingRepository : GreetingRepositoryInterface {
override fun greet(): String {
return "Hello from Repository"
}
}
モックアップ
モックアップも、インターフェイス(プロトコル)を実装したクラスになるように修正します。
class GreetingRepositoryMock : GreetingRepositoryProtocol {
func greet() -> String {
return "Hello from Mock"
}
}
class GreetingRepositoryMock : GreetingRepositoryInterface {
override fun greet(): String {
return "Hello from Mock"
}
}
再び出力用コードを実行してみると...
結果
Hello from Mock
モックアップからの出力に変わりました!
本物を使いたい場合も、以前のように
var viewModel = GreetingViewModel(greetingRepository: GreetingRepository())
print(viewModel.greet())
val viewModel = GreetingViewModel(greetingRepository = GreetingRepository())
println(viewModel.greet())
とするだけで、ViewModel
に変更を加えることなく、差し替えが可能になっています。
このように、Dependency Injection (依存性の注入) と インターフェイス (プロトコル) を合わせて使うことで、なかなか便利になったのが体感できたのではないかと思います。
これは、依存クラスの任意の挙動を再現する必要があるユニットテストなどに於いて、非常に有効なアプローチにもなります。
ユニットテスト
現在時刻に対応した挨拶を返すViewModel
のgreet
メソッドを考えてみます。
仕様としては
- 6 ~ 11時 : "Good Morning"
- 12 ~ 17時 : "Good Afternoon"
- 18 ~ 翌5時 : "Good Evening"
を返すことにします。
実装してみるとこんな感じでしょうか。
protocol TimeProviderProtocol {
func hour() -> Int
}
class TimeProvider : TimeProviderProtocol {
func hour() -> Int {
return Calendar.current.component(.hour, from: Date())
}
}
class GreetingViewModel {
var timeProvider: TimeProviderProtocol
init(timeProvider: TimeProviderProtocol) {
self.timeProvider = timeProvider
}
func greet() -> String {
switch self.timeProvider.hour() {
case (let hour) where (6 <= hour && hour <= 11):
return "Good Morning"
case (let hour) where (12 <= hour && hour <= 17):
return "Good Afernoon"
case (let hour) where (18 <= hour && hour <= 23) || (0 <= hour && hour <= 5):
return "Good Evening"
default:
return "Hello"
}
}
}
var viewModel = GreetingViewModel(timeProvider: TimeProvider())
print(viewModel.greet())
import java.util.Calendar
import java.util.TimeZone
interface TimeProviderInterface {
fun hour(): Int
}
class TimeProvider : TimeProviderInterface {
override fun hour(): Int {
val calendar = Calendar.getInstance().apply {
timeZone = TimeZone.getTimeZone("Asia/Tokyo")
}
return calendar.get(Calendar.HOUR)
}
}
class GreetingViewModel(private val timeProvider: TimeProviderInterface) {
fun greet(): String {
return when (this.timeProvider.hour()) {
in 6..11 -> "Good Morning"
in 12..17 -> "Good Afternoon"
in 18..23, in 0..5 -> "Good Evening"
else -> "Hello"
}
}
}
val viewModel = GreetingViewModel(timeProvider = TimeProvider())
println(viewModel.greet())
このViewModel
が仕様通りに動くかどうかテストしたい場合、どうしますか?
わざわざその時間になるまで待って動かしますか?
それはさすがにありえないので、
任意の値を返せるTimeProvider
のモックアップを作ってテストします。
こんな感じでしょうか。
class TimeProviderMock : TimeProviderProtocol {
private var _hour: Int
init(hour: Int) {
self._hour = hour
}
func hour() -> Int {
return self._hour
}
}
class TimeProviderMock(private val hour: Int) : TimeProviderInterface {
override fun hour(): Int {
return this.hour
}
}
コンストラクタに渡した値がそのままhour
メソッドの戻り値になります。
とすると、テストコードをこんな感じで書けます。
(本来は境界値とかもチェックすべきですが省略。)
var viewModel: GreetingViewModel
viewModel = GreetingViewModel(timeProvider: TimeProviderMock(hour: 6))
assert(viewModel.greet() == "Good Morning")
viewModel = GreetingViewModel(timeProvider: TimeProviderMock(hour: 12))
assert(viewModel.greet() == "Good Afernoon")
viewModel = GreetingViewModel(timeProvider: TimeProviderMock(hour: 18))
assert(viewModel.greet() == "Good Evening")
import org.junit.Assert
var viewModel: GreetingViewModel
viewModel = GreetingViewModel(timeProvider = TimeProviderMock(hour = 6))
Assert.assertEquals("Good Morning", viewModel.greet())
viewModel = GreetingViewModel(timeProvider = TimeProviderMock(hour = 12))
Assert.assertEquals("Good Afternoon", viewModel.greet())
viewModel = GreetingViewModel(timeProvider = TimeProviderMock(hour = 18))
Assert.assertEquals("Good Evening", viewModel.greet())
わざわざ時間になるのを待つことなく、そしてViewModel
を修正することなく、
ViewModel
のユニットテストを実行できるようになりました!
DI (Dependency Injection) って便利ですね!
以上です。