[Swift] TCAのDependencyValues風の依存注入でSingleton地獄から脱却する

## はじめに
iOSアプリ開発において、Singletonに依存した設計が蔓延してしまうことはないでしょうか？  
便利な反面、テスト性の低下や変更耐性の低下、依存の隠蔽など多くの問題を引き起こします。

本記事では、[The Composable Architecture (TCA)](https://github.com/pointfreeco/swift-composable-architecture) の `DependencyValues`の設計思想をベースに、依存を構造化し、既存のSingletonを段階的に置き換えていく方法を紹介します。

## なぜSingleton地獄になるのか？

### 1. Swiftには標準のDI機構が存在しない
Swiftには、Kotlinの[Hilt](https://developer.android.com/training/dependency-injection/hilt-android)のような依存性注入（DI）フレームワークが存在しません。基本的には `init()` による手動注入が求められます。

- ライフサイクルに応じたスコープ制御も自前で実装する必要がある
    
- Viewの入れ子構造により注入の伝播が煩雑になる
    

結果として「手軽なSingletonで済ませる」という判断がなされがちです。

### 2. 設計コストゼロで使える誘惑
`Class.shared` のように、どこからでも簡単にアクセスできるため、依存設計を省略しやすくなります。その結果、以下のような問題が発生します。

- モック化が困難
- 依存関係がコード上に現れない
- 状態がグローバルに共有され、予期せぬ副作用が生じる

### Singletonの問題点まとめ
| 問題点        | 説明                            |
| ---------- | ----------------------------- |
| 依存が暗黙的     | どのクラスが何に依存しているか分からず、テストも難しい   |
| テスト困難      | 状態が共有されるため、並列テストやモックの差し替えが難しい |
| グローバル状態の汚染 | スレッド不安全、状態の競合、副作用の伝播          |
| 密結合        | 型が固定され差し替えや再利用が困難             |


## TCAに学ぶ依存注入の考え方
TCAに組み込まれている[swift-dependencies](https://github.com/pointfreeco/swift-dependencies)ライブラリでは、`DependencyValues`を使って依存注入を構造化します。

このライブラリを使えば、下記のような簡単な構文で依存を参照できます。

```swift
@Dependency(\.apiClient) var apiClient
```
また、`.withDependencies {}` を使えば、一時的にモックなどに差し替えることも可能です。

```swift
withDependencies {
  $0.apiClient = MockApiClient()
} operation: {
  // テスト実行
}
```

## DependencyValuesで得られる主なメリット

- **依存の明示**：コード上に明示的に依存が現れる（`@Dependency`）
- **テスト容易**：一時的にモックへ差し替えられる（`withDependencies`）
- **スコープ制御**：`Task`ごとに異なる依存を持たせられる
- **段階導入可能**：既存のSingletonから柔軟に移行できる

## DependencyValues の自作方法
TCAの DependencyValues の考え方を模倣し、自前で実装することで、外部ライブラリに依存せずにDIの構造を導入できます。これにより、既存のコードベースにも柔軟に適用できます。

以下の記事が大変参考になりました！

https://zenn.dev/prevent/articles/a874e3660b08d2

この自作版は、`DependencyValues`の仕組みを簡易的に再現するものです。

### 1. `DependencyKey`の定義
```swift:DependencyKey.swift
protocol DependencyKey {
    // 依存の型
    associatedtype Value: Sendable
    // 本番用のデフォルト依存（登録がなければこれが使われる）
    static var liveValue: Value { get }
}
```
### 2. `DependencyValues`の定義
```swift:DependencyValues.swift
struct DependencyValues: Sendable {
    // 現在のコンテキストを限定した依存セット。withDependency内で差し替えられる
    @TaskLocal static var current = DependencyValues()
    // 実際の依存を保持するDictionary
    private var storage: [ObjectIdentifier: any Sendable] = [:]
    // 依存を取得・設定するsubscript
    subscript<K>(key: K.Type) -> K.Value where K : DependencyKey { 
        get {
            // 指定Keyに対応する依存が保存されていればそれを取り出す
            guard let base = storage[ObjectIdentifier(key)],
                  let dependency = base as? K.Value else {
                // なければそのキーのliveValue（デフォルト）を返す
                return key.liveValue
            }
            return dependency
        }
        
        set {
            // 指定キーに新たな依存を保存
            storage[ObjectIdentifier(key)] = newValue
        }
    }
}

// 一時的に依存を差し替えて操作を実行するメソッド
extension DependencyValues {
    // 同期処理での依存差し替え
    static func withDependency<R>(_ setDependency: (inout DependencyValues) -> Void, operation: () -> R) -> R {
        var currentDependencyValue = DependencyValues.current
        // 差し替えを適用
        setDependency(&currentDependencyValue)
        // 差し替えた依存でoperationを実行（Taskローカルで一時的に切り替え）
        return DependencyValues.$current.withValue(currentDependencyValue) { operation() }
    }
    
    // 非同期処理での依存差し替え
    static func withDependency<R>(_ setDependency: (inout DependencyValues) -> Void, operation: () async -> R) async -> R {
        var currentDependencyValue = DependencyValues.current
        setDependency(&currentDependencyValue)
        return await DependencyValues.$current.withValue(currentDependencyValue) { await operation() }
    }
}
```

### 3. `@Dependency`プロパティラッパーの定義
```swift:Dependency.swift
@propertyWrapper
struct Dependency<Value> {
    // ラッパー越しに依存を取得可能に（@Dependency(\.xxx) var xxx で使える）
    var wrappedValue: Value { DependencyValues.current[keyPath: self.keyPath] }
    // 参照するKeyPath（例: \.apiClient）
    private let keyPath: KeyPath<DependencyValues, Value> & Sendable
    
    init(_ keyPath: KeyPath<DependencyValues, Value> & Sendable) {
        self.keyPath = keyPath
    }
}
```

## 使用例

### 1. 本番用の依存クライアント
```swift:LoggerClient.swift
struct LoggerClient: Sendable {
    let log: @Sendable (String) -> Void
}

extension LoggerClient {
    static let live = LoggerClient { message in
        print("[LIVE] \(message)")
    }

    static let mock = LoggerClient { message in
        print("[MOCK] \(message)")
    }
}

enum LoggerKey: DependencyKey {
    static let liveValue: LoggerClient = .live
}

extension DependencyValues {
    var logger: LoggerClient {
        get { self[LoggerKey.self] }
        set { self[LoggerKey.self] = newValue }
    }
}
```

### 2. 使用箇所での注入
```swift:LoggerUser.swift
struct LoggerUser {
    @Dependency(\.logger) var logger

    func run() {
        logger.log("実行しました")
    }
}

let user = LoggerUser()
user.run()
// -> [LIVE] 実行しました
```

### 3. モックでのテスト
```swift:LoggerUserTests.swift
let user = DependencyValues.withDependency {
    $0.logger = .mock
} operation: {
    LoggerUser()
}

user.run()
// -> [MOCK] 実行しました
```
## Sendable 対応と構造化の注意点

`DependencyValues`をTCAのようにスレッドセーフ・並列テスト可能に保つには、依存にSendableを意識した設計が必要です。

* 依存は基本`struct`か`actor`
* クロージャには `@Sendable` 明示
* どうしても`class`が必要なら`@unchecked Sendable`を使う（非推奨）

:::note info
これらの注意点は、Swift 6 移行のために必要なアプローチともいえます。
:::

## Singleton -> `DependencyValues`移行ステップ
### Step1. Singletonクラスをstructでラップして置き換える
```swift:UserDefaultsManager.swift
// 既存コード（Before）

import Foundation

final class UserDefaultsManager {
    static let shared = UserDefaultsManager()
    private init() {}

    func save(_ value: String, for key: String) {
        UserDefaults.standard.set(value, forKey: key)
    }
    
    func load(for key: String) -> String? {
        UserDefaults.standard.string(forKey: key)
    }
}
```
```swift:UserDefaultsClient.swift
// 書き替え後コード（After）

struct UserDefaultsClient {
    let save: (String, String) -> Void
    let load: (String) -> String?
}

extension UserDefaultsClient {
    static let live = UserDefaultsClient(
        save: UserDefaultsManager.shared.save,
        load: UserDefaultsManager.shared.load
    )
}

// sharedを隠蔽
private final class UserDefaultsManager {
    static let shared = UserDefaultsManager()
    private init() {}
    
    func save(_ value: String, for key: String) {  }
    func load(for key: String) -> String? { "" }
}

// Keyを設定して使用箇所で@Dependency宣言する
enum UserDefaultsClientKey: DependencyKey {
    static let liveValue = UserDefaultsClient.live
}

extension DependencyValues {
    var userDefaults: UserDefaultsClient {
        get { self[UserDefaultsClientKey.self] }
        set { self[UserDefaultsClientKey.self] = newValue }
    }
}
```

### Step2. テスト時にモックを注入できるように書き換える
```swift:UserDefaultsClient.swift
extension UserDefaultsClient {
    static let mock = UserDefaultsClient(
        save: { key, value in
            print("[MOCK] save \(key): \(value)")
        },
        load: { key in
            print("[MOCK] load \(key)")
            return "mocked_value"
        }
    )
}
```

### Step3. `shared`インスタンスを`actor`や`struct`にリファクタリングする（Sendable対応）
```swift:UserDefaultsClient.swift
struct UserDefaultsClient {
    // クロージャをSendableにする
    let save: @Sendable (String, String) -> Void
    let load: @Sendable (String) -> String?
}

extension UserDefaultsClient {
    // sharedを削除
    static let live = Self(
        save: { key, value in
            UserDefaults.standard.set(value, forKey: key)
        },
        load: { key in
            UserDefaults.standard.string(forKey: key)
        }
    )
}
```
これで完全に`DependencyValues`に移行でき、Swift 6にも対応できるようになりました。
実際にはもっと色々な状態をSingletonに保持しているかと思うので、簡単にはいかないと思いますが、頑張って置き換えていきましょう。

**徐々に差し替え可能**という点が`DependencyValues`の最大のメリットです。

## まとめ
- Swiftは言語的にDIを支援しておらず、Singletonに依存しやすい構造になっている
- `DependencyValues`を用いれば、既存の設計を壊さずに段階的な移行が可能
- `@Dependency`や`withDependency`によって、テスト可能・スコープ制御可能な構造を自然に導入できる

もし現場でSingletonの多用に悩んでいる方がいれば、`DependencyValues`から始めてみることをおすすめします。

静的アクセスの便利さを維持しつつ、疎結合・スケーラブル・テスト可能な構造を段階的に実現できます。

## 参考
* [swift-dependencies](https://github.com/pointfreeco/swift-dependencies)
* [pointfreeco/swift-composable-architecture](https://github.com/pointfreeco/swift-composable-architecture)
* [TaskLocal | Apple Developer Documentation](https://developer.apple.com/documentation/swift/tasklocal)
* [Swift Concurrency - Documentation](https://docs.swift.org/swift-book/documentation/the-swift-programming-language/concurrency/)
* [Swiftで実装する基本的な依存性注入](https://zenn.dev/prevent/articles/a874e3660b08d2)