SwiftのProperty Wrapperを利用してDIする

SwiftでDependency Injectionをするとき、 [Swinject](https://github.com/Swinject/Swinject)や[needle](https://github.com/uber/needle)のようなライブラリで実現する方法がありますが、SwiftLee先生がProperty Wrapperを利用する例を紹介していました。

https://www.avanderlee.com/swift/dependency-injection/

結果的に少ないコードでDIができ、自分好みの方法だったのでアレンジしてみました。

```swift:DependencyInjection.swift
import Foundation

// Inspired by SwiftLee
// https://www.avanderlee.com/swift/dependency-injection/

// この型にextensionでDI定義を羅列する(後述)
public class DependencyObjects {
    init() {}
    
    public static subscript<T>(_ dependency: DependencyObject<T>) -> T {
        get { dependency.object }
        set { dependency.object = newValue }
    }
}

// DependencyObjects内に定義する、DIする型とオブジェクト生成
public final class DependencyObject<T>: DependencyObjects {
    // DependencyObjectにする型のイニシャライザでも参照できるよう、遅延生成する
    fileprivate lazy var object = builder()
    private let builder: () -> T
    
    public init(_ builder: @escaping () -> T) {
        self.builder = builder
        super.init()
    }
}

// 実際にDIをするためのProperty Wrapper
@propertyWrapper
public struct Injected<T> {
    private let object: T
    
    public init(_ dependency: DependencyObject<T>) {
        self.object = DependencyObjects[dependency]
    }

    public var wrappedValue: T { object }
}
```

## 使用例

拙作のアプリで実際に書いているコードで紹介したいと思います。
まず、DIに用いるオブジェクトを `DependencyObjects` に追加します。

```swift:Dependencies.swift
extension DependencyObjects {
    static let purchaseStore = DependencyObject<PurchaseStore> { DefaultPurchaseStore() }

    static let subscriptionUseCase = DependencyObject<SubscriptionUseCase> {
        DefaultSubscriptionUseCase(purchaseStore: DependencyObjects[.purchaseStore])
    }
}
```

ここで追加した `purchaseStore` と `subscriptionUseCase` がDIできるようになります。

次に課金(サブスクリプション)のオファーをする画面の実装ですが、以下のような構造になっています

* SubscriptionOfferPresenter: 画面のビューモデル。 `SubscriptionUseCase` に依存
* SubscriptionUseCase: 課金まわりの機能を提供するユースケース。実際の課金ロジックは `PurchaseStore` にある
* PurchaseStore: StoreKitを通じて課金まわりの処理を行う

### SubscriptionOfferPresenter

サブスクリプションまわりの機能を提供する `SubscriptionUseCase` をDIしています。

```swift:SubscriptionOfferPresenter.swift

// 説明のため、実際のコードより一部省略しています

@MainActor
final class SubscriptionOfferPresenter: ObservableObject {
    init() {
    }

    /// UseCaseをDI
    @Injected(.subscriptionUseCase)
    private var subscriptionUseCase: SubscriptionUseCase

    // MARK: - Output

    enum State {
        case unprepared
        case prepared(SubscriptionProduct)
        case subscribed
    }
    @Published private(set) var state: State = .unprepared
    @Published private(set) var hud: HUD = .none
    @Published private(set) var failureAlert: FailureAlert?

    /// 提供するサブスクリプション情報の読み込み等の準備
    func prepare() {
        Task {
            hud = .loading(true)
            do {
                let product = try await subscriptionUseCase.product(for: .premium)
                state = .prepared(product)
            } catch {
                guard !Task.isCancelled else { return }
                failureAlert = .init(error: error)
            }
            hud = .loading(false)
        }
    }

    /// サブスクリプションの購入    
    func subscribe() {
        guard case .prepared(let product) = state else {
            return
        }
        
        Task {
            hud = .processing(true)
            do {
                try await subscriptionUseCase.subscribe(product: product)
                state = .subscribed
                failureAlert = nil
            } catch {
                guard !Task.isCancelled else { return }
                if
                    let subscriptionError = error as? SubscriptionError,
                    case .cancelled = subscriptionError
                {
                    // キャンセルは無視
                } else {
                    failureAlert = .init(error: error)
                }
            }
            hud = .processing(false)
        }
    }
}
```

### SubscriptionUseCase

`PurchaseStore` に対しても `@Injected` を使いたいところですが、**依存関係逆転の原則(DIP)の関係にある** と考えると、ここはイニシャライザを介して渡すのが良いのかなと思います。
上記 `DependencyObjects` の中で依存関係の解決しているので、この形でも利用時の手間はあまりないのかなと。

```swift:SubscriptionUseCase.swift

protocol SubscriptionProduct { ...省略... }
protocol SubscriptionUseCase: AnyObject { ...省略... }
enum SubscriptionError: Error { ...省略... }

extension Notification.Name {
    static let subscriptionDidUpdateNotification = Notification.Name("subscriptionDidUpdateNotification")
}

// MARK: - Implementation

final class DefaultSubscriptionUseCase: SubscriptionUseCase {
    init(purchaseStore: PurchaseStore) {
        self.purchaseStore = purchaseStore
    }

    private let purchaseStore: PurchaseStore

    /// 指定のplanに対するサブスクリプションを取得
    func product(for plan: SubscriptionPlan) async throws -> SubscriptionProduct {
        guard let product = try await purchaseStore.subscriptionProducts(for: [plan.productID]).first else {
            throw SubscriptionError.notFound(plan)
        }
        
        return product
    }
    
    /// サブスクリプションの購入
    func subscribe(product: SubscriptionProduct) async throws {
        guard try await purchaseStore.subscribe(product: product) else {
            throw SubscriptionError.cancelled
        }
        
        // サブスクリプションの更新を通知
        NotificationCenter.default
            .post(name: .subscriptionDidUpdateNotification, object: nil)
    }
}
```


### Unit Test

ビューモデルのテストコードを書くときは、 `DependencyObjects` の `subscript` を通じてデフォルトのオブジェクトを入れ替えます。

```swift: SubscriptionOfferPresenterTests.swift
class SubscriptionOfferPresenterTests: XCTestCase {
    private var presenter: SubscriptionOfferPresenter!
    override func setUpWithError() throws {
        // ビューモデルをインスタンス化する前に、DIしているオブジェクトを入れ替えておく
        DependencyObjects[.subscriptionUseCase] = SubscriptionUseCaseStub()
        presenter = SubscriptionOfferPresenter()
    }

    func testPrepare() async throws {
        presenter.subscribe()
        // TODO: XCTAssert
    }
}
```

### まとめ

ライブラリを使用しなくても比較的簡単にDIが導入でき、かつ依存オブジェクトの引き渡しが無い分、シンプルなコードになるのではないかと思います。
(アプリケーションの実装や挙動を主眼におきつつ、テスタブルにもしているという感じでしょうか)

一方で `@Injected` を導入した型はコードとしての独立性は低下するので、従来どおりイニシャライザで依存オブジェクトを引き渡す方が良いという見方も理解できます。

他にも「型パラメータインジェクション」というやり方もあります。
https://qiita.com/uhooi/items/16aa67b44e2614c8d7b9

プロジェクトの指向に合わせて、どういう形でDIを実現するのか考えることが大切ですね！