
DI (Dependency Injection) を学ぶ機会があったので備忘録として記事を書こうと思いました
Dependency Injectionとは?
【Dependency Injection】= 依存性注入
Dependency injection means giving an object its instance variables. Really. That’s it.
(依存性注入とは、オブジェクトをインスタンス変数(定数)に渡すこと、ただそれだけです) *海外サイトより
文字通り依存性を注入する、という意味合いですがこれだけではイメージし辛いと思うのでサンプルコードでざっくりみてみましょう。
1. ラーメン作成プロセスで学ぶDI
- スープ
- 麺のタイプ
この二つを決めて、ラーメンを作るcookRamen関数を考えてみる。
1.1 DIせずに直接指定するバージョン
struct TonkotsuSoup {
let taste: String = "豚骨スープ"
}
struct HomemadeNoodle {
let noodle: String = "自家製麺"
}
// DIをしない場合
final class TanakaRamen {
private let soup = TonkotsuSoup()
private let noodle = HomemadeNoodle()
func cookRamen() {
print("\(soup.taste)と\(noodle.noodle)のラーメンを作ります")
}
}
let tanakaRamen = TanakaRamen()
tanakaRamen.cookRamen() // 結果: 豚骨スープと自家製麺のラーメンを作ります
ポイント:
- 直接依存関係を生成していので、他のスープや麺に変更できない
- 魚介系スープ & ヤマダ製麺にしたいとなったら、新規でclassを生成する必要がある
- テストもしづらい
つまりいいところなし、他のラーメンを作る手間が増えるので、ここでDIの登場。
スープ、麺は後から注入するという考え。
1.2 DIであとからスープと麺を決定するバージョン
protocol Soup {
var taste: String { get }
}
protocol Noodle {
var style: String { get }
}
final class TanakaRamen {
private let soup: Soup
private let noodle: Noodle
init(soup: Soup, noodle: Noodle) {
self.soup = soup
self.noodle = noodle
}
func cookRamen() {
print("\(soup.taste)と\(noodle.style)のラーメンを作ります")
}
}
struct SeafoodSoup: Soup {
var taste: String { "魚介系スープ" }
}
struct YamadaNoodle: Noodle {
var style: String { "ヤマダ製麺" }
}
let tanakaRamen = TanakaRamen(soup: SeafoodSoup(), noodle: YamadaNoodle())
tanakaRamen.cookRamen() // 結果: 魚介系スープとヤマダ製麺のラーメンを作ります
ポイント:
- soup, noodleは後から注入するので、SpicySoup & YamadaNoodleとかにも簡単に差し替え可能
- Soup, Noodleプロトコルに準拠したものなら何でも入れられる
- テストもしやすい (TestSoup, TestNoodleにも簡単に入れ替えられる)
つまり将来を見据えたTestability, 取り替えのしやすさを考慮するとDIで依存性を渡してあげるのがとてもGOODということ。
次は実例でDIを見ていきましょう。
2. 実例 (ViewModelに依存性を注入)
SignInReposioryProtocolに準拠した、SignInRepositoryを作ってDIを学んでみましょう。
struct User: Decodable {
let id: Int
let name: String
}
protocol SignInRepositoryProtocol {
func signIn(email: String, password: String) async throws -> User
}
final class SignInRepository: SignInRepositoryProtocol {
func signIn(email: String, password: String) async throws -> User {
return User(id: 1, name: "Taro Yamada") // 実際はAPI通信などを行う
}
}
@MainActor
final class SignInViewModel: ObservableObject {
@Published var email: String = ""
@Published var password: String = ""
@Published var user: User?
private let repository: SignInRepositoryProtocol
init(repository: SignInRepositoryProtocol) {
self.repository = repository
}
func signIn(email: String, password: String) async {
self.user = try? await repository.signIn(email: email, password: password) // 実際はtryでエラーハンドリングとかをしておくとGood
}
}
let repository = SignInRepository()
let viewModel = SignInViewModel(repository: repository)
// SignInRepositoryProtocolに準拠しているrepositoryなら容易に切り替えが可能なため、ViewModelのテストが容易になる
let testSignInRepository = TestSignInRepository()
let viewModel = SignInViewModel(repository: testSignInRepository)
DIをうまく活用することによって、無駄class, structの再生成をする必要がなくなったり、テスト用に依存性を簡単に切り替えられるため、ぜひ活用してみてください。