【Xcode/Swift】Dependency Injectionをざっくり学ぶ

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の再生成をする必要がなくなったり、テスト用に依存性を簡単に切り替えられるため、ぜひ活用してみてください。