TCA で依存性を逆転させる
モジュールが分かれてたほうが DIP のメリットを最大限受けれる (ビルド時間等) ので、マルチモジュール前提。
- インターフェイスとなるオブジェクトは
TestDependencyKeyだけに準拠しておく - インターフェイスとなるオブジェクトがあるモジュール内で
DependencyValuesに登録する- これにより実装を知らないでも
Dependencyから取ってこれる
- これにより実装を知らないでも
- 別モジュールとして
liveValueを提供してDependencyKeyに準拠する - それぞれは別 library (SPM の場合の話) として提供しておき、同じ library には入れないことで複雑な実装が頻繁にビルドされないようにする
- Xcode Project にインターフェイスが入った library と実装 (
liveValue) が入った library を依存するようにし、 Xcode Target のビルドで初めて依存解決が行われるようにすることで、完全に (Package 内部では) 分離できる。
iso-words の実装を見ればわかる
ApiClient module と ApiClientLive module を見るのが一番分かりやすい: Package.swift
ApiClient がインターフェイスになっていて、 ApiClientLive にて ApiClient を extension して liveValue を実装している。
また、モジュール間の依存関係から見ても DIP できていて、以下の図のようになっている。
flowchart TD
xcode[isowords.xcodeproj] --> AppFeature
xcode --> ApiClientLive
subgraph Swift Package
subgraph Core library
AppFeature --> ApiClient
AppFeature --> OtherFeatures
OtherFeatures --> ApiClient
end
subgraph Live library
direction BT
ApiClientLive -- add liveValue --> ApiClient
end
endApiClientはどこにも依存していない- 実際には
SharedModulesや swift-dependencies 等のいくつかのライブラリに依存しているが、置いておく
- 実際には
- 各 Feature (アプリケーション層) は
ApiClientの軽量なインターフェイスに依存している ApiClientLiveが実際の処理であるliveValueを提供し、 Infrastructure 層的な役割になっている- Infra は
AppFeatureライブラリからは見えない liveValueの在り処を知っているのはisowords.xcodeprojだけ。- Xcode Project が最終的な DI コンテナ的な役割になっている
- 基本的には
AppFeatureにすべて詰め込まれているので、 Xcode Project への依存もきれい
swift-dependencies の実装から考える
Dependency として登録するには、 DependencyValues の subscript で取ってこれるようにする必要があった。
public struct DependencyValues: Sendable {
public subscript<Key: TestDependencyKey>(
key: Key.Type,
file: StaticString = #file,
function: StaticString = #function,
line: UInt = #line
) -> Key.Value where Key.Value: Sendable { ... }
}
DependencyValues の subscript としての制約は TestDependencyKey であるというのが肝。
TestDependencyKey と DependencyKey の定義を見る。
public protocol TestDependencyKey {
associatedtype Value: Sendable = Self
static var previewValue: Value { get }
static var testValue: Value { get }
}
public protocol DependencyKey: TestDependencyKey {
static var liveValue: Value { get }
associatedtype Value = Self
static var previewValue: Value { get }
static var testValue: Value { get }
}
つまり以下の流れで分離できるというのが、仕組みからも分かる。
@Dependency(keyPath)で使えるようにするにはDependencyValuesの subscript に型が適合する必要がある- →
DependencyValues.subscriptの key はTestDependencyKeyである必要がある - →
TestDependencyKeyにはliveValue(具体的な実装) は不要- よって別モジュールに実装を分けられる
- →
liveValueについては別モジュールにextensionして追加する- このとき合わせて
DependencyKeyに準拠しておく
- このとき合わせて
- → 具体的な実装無しで I/F が独立させることが可能になった
