Kotlin
Swift

DI (Dependency Injection) の基本

DI (Dependency Injection) とは何なのか?
そして、なぜ便利なのかを簡単に説明しようと思います。

他のクラスに依存していない場合

まずは何者にも依存していない簡単なViewModelを作ってみます。

ViewModel

swift
class GreetingViewModel {
    func greet() -> String {
        return "Hello"
    }
}
kotlin
class GreetingViewModel {
    fun greet(): String {
        return "Hello"
    }
}

greetメソッドが返す文字列を出力するコードはとりあえずこんな感じ。

swift
var viewModel = GreetingViewModel()

print(viewModel.greet())
kotlin
val viewModel = GreetingViewModel()

println(viewModel.greet())

こいつらを何かしらの方法(SwiftPlaygroundやTry Kotlin等)で動かしてみると...

結果

Hello

という感じでHelloと出力されます。
当たり前ですね。

他のクラスに依存している場合

greetメソッドが返す文字列を、他のクラスに依存するようにしてみます。

依存するクラス

swift
class GreetingRepository {
    func greet() -> String {
        return "Hello from Repository"
    }
}
kotlin
class GreetingRepository {
    fun greet(): String {
        return "Hello from Repository"
    }
}

ViewModelがこのクラスに依存するようにします。

ViewModel

swift
class GreetingViewModel {

    private var greetingRepository = GreetingRepository()

    func greet() -> String {
        return self.greetingRepository.greet()
    }
}
kotlin
class GreetingViewModel {

    private val greetingRepository = GreetingRepository()

    fun greet(): String {
        return this.greetingRepository.greet()
    }
}

結果

Hello from Repository

GreetingRepositoryクラスから返ってきた文字列が表示されました。
問題なしですね。

依存するクラスを外から与える

では、このGreetingViewModelクラスの中で生成しているGreetingRepositoryのインスタンスを
クラスの外で生成してGreetingViewModelクラスに与えてみます。

ViewModel

ViewModelクラスのコンストラクタ引数でGreetingRepositoryのインスタンスを受け取るように修正します。

swift
class GreetingViewModel {
    private var greetingRepository: GreetingRepository

    init(greetingRepository: GreetingRepository) {
        self.greetingRepository = greetingRepository
    }

    func greet() -> String {
        return self.greetingRepository.greet()
    }
}
kotlin
class GreetingViewModel(private val greetingRepository: GreetingRepository) {
    fun greet(): String {
        return this.greetingRepository.greet()
    }
}

そうすると出力用コードはこんな感じになるかと思います。

swift
var viewModel = GreetingViewModel(greetingRepository: GreetingRepository())

print(viewModel.greet())
kotlin
val viewModel = GreetingViewModel(greetingRepository = GreetingRepository())

println(viewModel.greet())

これを実行してみると...

結果

Hello from Repository

先ほどと同じ出力になり、外から依存クラスのインスタンスを与えた場合でも同じ挙動になりました。

これを Dependency Injection (依存性の注入) と呼びます。

ただ、この状態ではクラス内でインスタンス化するのとほとんど変わりません。
なにか利点はあるのでしょうか?

たとえば、注入するGreetingRepositoryがまだ完成していないとしたらどうでしょう?

インターフェイス(プロトコル)の活用

まだ完成していないクラスを使いたいとなったら、モックアップになるクラスを作って本物の代わりをさせます。

こんな感じでしょうか。

モックアップ

swift
class GreetingRepositoryMock {
    func greet() -> String {
        return "Hello from Mock"
    }
}
kotlin
class GreetingRepositoryMock {
    fun greet(): String {
        return "Hello from Mock"
    }
}

では出力用コードをこんな風に書き換えて...

swift
var viewModel = GreetingViewModel(greetingRepository: GreetingRepositoryMock())

print(viewModel.greet())
kotlin
val viewModel = GreetingViewModel(greetingRepository = GreetingRepositoryMock())

println(viewModel.greet())

あれっ!?
「型が違います」みたいなこと言われてビルドできない!!

そうなんです。
GreetingRepositoryGreetingRepositoryMockでは型が違うので引数として渡せないんです。

かと言って、引数の型をGreetingRepositoryMockにしてしまうと、クラス完成後にまた修正しなくてはなりません。

そこでインターフェイス(プロトコル)を使います。

インターフェイス(プロトコル)というのは、クラスが実装すべきメソッドやプロパティを定義する型枠です。

インターフェイス(プロトコル)

クラスにはgreetというメソッドがあれば良いので、こんな感じになります。

swift
protocol GreetingRepositoryProtocol {
    func greet() -> String
}
kotlin
interface GreetingRepositoryInterface {
    fun greet(): String
}

ViewModel

引数の型を、クラスではなくインターフェイス(プロトコル)に変更します。
こうしておくと、インターフェイス(プロトコル)を実装したクラスであれば何でも渡せるようになります。

swift
class GreetingViewModel {
    private var greetingRepository: GreetingRepositoryProtocol

    init(greetingRepository: GreetingRepositoryProtocol) {
        self.greetingRepository = greetingRepository
    }

    func greet() -> String {
        return self.greetingRepository.greet()
    }
}
kotlin
class GreetingViewModel(private val greetingRepository: GreetingRepositoryInterface) {
    fun greet(): String {
        return this.greetingRepository.greet()
    }
}

GreetingRepositoryクラス

インターフェイス(プロトコル)を実装したクラスになるように修正します。

swift
class GreetingRepository : GreetingRepositoryProtocol {
    func greet() -> String {
        return "Hello from Repository"
    }
}
kotlin
class GreetingRepository : GreetingRepositoryInterface {
    override fun greet(): String {
        return "Hello from Repository"
    }
}

モックアップ

モックアップも、インターフェイス(プロトコル)を実装したクラスになるように修正します。

swift
class GreetingRepositoryMock : GreetingRepositoryProtocol {
    func greet() -> String {
        return "Hello from Mock"
    }
}
kotlin
class GreetingRepositoryMock : GreetingRepositoryInterface {
    override fun greet(): String {
        return "Hello from Mock"
    }
}

再び出力用コードを実行してみると...

結果

Hello from Mock

モックアップからの出力に変わりました!
本物を使いたい場合も、以前のように

swift
var viewModel = GreetingViewModel(greetingRepository: GreetingRepository())

print(viewModel.greet())
kotlin
val viewModel = GreetingViewModel(greetingRepository = GreetingRepository())

println(viewModel.greet())

とするだけで、ViewModelに変更を加えることなく、差し替えが可能になっています。

このように、Dependency Injection (依存性の注入)インターフェイス (プロトコル) を合わせて使うことで、なかなか便利になったのが体感できたのではないかと思います。

これは、依存クラスの任意の挙動を再現する必要があるユニットテストなどに於いて、非常に有効なアプローチにもなります。

ユニットテスト

現在時刻に対応した挨拶を返すViewModelgreetメソッドを考えてみます。

仕様としては

  • 6 ~ 11時 : "Good Morning"
  • 12 ~ 17時 : "Good Afternoon"
  • 18 ~ 翌5時 : "Good Evening"

を返すことにします。

実装してみるとこんな感じでしょうか。

swift
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())
kotlin
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のモックアップを作ってテストします。

こんな感じでしょうか。

swift
class TimeProviderMock : TimeProviderProtocol {

    private var _hour: Int

    init(hour: Int) {
        self._hour = hour
    }

    func hour() -> Int {
        return self._hour
    }
}
kotlin
class TimeProviderMock(private val hour: Int) : TimeProviderInterface {
    override fun hour(): Int {
        return this.hour
    }
}

コンストラクタに渡した値がそのままhourメソッドの戻り値になります。

とすると、テストコードをこんな感じで書けます。
(本来は境界値とかもチェックすべきですが省略。)

swift
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")
kotlin
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) って便利ですね!

以上です。