7
1

swift-dependenciesのMacroでいろいろビミョかった問題が解決されてた

Posted at

3週間前にDependenciesのバージョンが1.1.0に更新され、マクロが使えるようになりました。これにより、以前は少し微妙だなと感じていた点がマクロによって解決されていたので、今回はそれについて解説していきます。

今回追加されたマクロはprotocolベースのDIには使用できません。最初にstructベースのDIがなぜ推奨されているのか、から解説します。知っている方は1.1.0以前のDependenciesまで飛ばしてください。

なぜstructベースのDIが推奨されているのか

前提として、swift-dependenciesではprotocolベースのDIではなく、structベースのDIが推奨されています。

struct APIClient {
  var fetch: @Sendable (_ query: String) async throws -> Response
  var post: @Sendable (_ post: Post) async throws -> Void
}

これにはいくつかメリットがあります。
それは、ある機能で必要なエンドポイントのみを指定できるという点です。

final class FeatureModel: ObservableObject {
  @Dependency(\.apiClient.fetch) var fetch
}

これは機能をわかりやすく、使いやすくするだけでなく、テスト時にもメリットが受けられます。

プロトコルベースのDIでは、プロトコルに準拠するオブジェクト全体をモック化する必要があるため、テスト時に必要のないエンドポイントまでオーバーライドする必要がありました。

しかし構造体ベースのDIだと、特定の依存関係のエンドポイントのみ指定することができ、他のエンドポイントはオーバーライドする必要がなくなります。

func testFeature() {
  let model = withDependencies {
    // 特定の機能のみオーバーライドしている
    // APIClient.postはFeatureModelで使っていないのでオーバーライドする必要ない
    $0.apiClient.fetch = { _ in Response(/* ... */)) }
  } operation: {
    FeatureModel()
  }
  ...
}

また、testValueをunimplementedにしておくことで、テスト時にオーバーライドしていないエンドポイントが呼び出された場合に、テストが失敗するようになります。

extension APIClient: DependencyKey {
  static var liveValue: Self = Self(/* ... */)

  static let previewValue = Self(/* ... */)

  static let testValue = Self(
    fetch: unimplemented("APIClient.fetch"),
    post: unimplemented("APIClient.post")
  )
}

テストが成功すれば、依存関係の他のエンドポイントが使用されていないことが保証されます。

v1.1.0以前のDependencies

structベースのDIにすることにより、さまざまなメリットが享受できますが、これにはデメリットもありました。

その1

関数ではなくクロージャで定義していることにより、引数ラベルが使えません。

public struct APIClient {
  public var fetch: @Sendable (_ query: String) async throws -> Response
  public var post: @Sendable (_ post: Post) async throws -> Void
}

@Dependency(\.apiClient.fetch) var fetch

let response = try await fetch("ほげ")

引数ラベルがないと、この引数が何を意味するのかがパッとみてわかりません。

その2

public initを書くのが面倒です。APIClientはプロパティが2つなのであまり面倒ではないですが、これが20個とかに増えたら毎回追加するのは面倒です。

public struct APIClient {
  public var fetch: @Sendable (_ query: String) async throws -> Response
  public var post: @Sendable (_ post: Post) async throws -> Void
  ...

  public init(
    fetch: @escaping @Sendable (_ query: String) async throws -> Response
    post: @escaping @Sendable (_ post: Post) async throws -> Void
  ) {
    self.fetch = fetch
    self.post = post
  }
}

その3

unimplementedの実装が面倒です。これもプロパティが20個とかに増えたら面倒ですよね。

extension APIClient: DependencyKey {
  public static var liveValue: Self = Self(/* ... */)

  public static let previewValue = Self(/* ... */)

  static let testValue = Self(
    fetch: unimplemented("APIClient.fetch"),
    post: unimplemented("APIClient.post")
  )
}

v1.1.0以降のDependencies

なんと、これら3つの面倒なポイントがすべてマクロによって解決されています!!

@DependencyClient
public struct APIClient {
+ @DependencyEndpoint
  public var fetch: @Sendable (_ query: String) async throws -> Response
+ @DependencyEndpoint
  public var post: @Sendable (_ post: Post) async throws -> Void

+ public init(
+   fetch: @Sendable @escaping (_ query: String) async throws -> Response,
+   post: @Sendable @escaping (_ post: Post) async throws -> Void
+ ) {
+   self.fetch = fetch
+   self.post = post
+ }
}

DependencyClientマクロでは、各エンドポイントに@DependencyEndpointを付与しています。
また、public initも生成しています。

そして、@DependencyEndpointが付与されたエンドポイントをExpandすると、以下のようになっています。

@DependencyClient
public struct APIClient {
  @DependencyEndpoint
  public var fetch: @Sendable (_ query: String) async throws -> Response
+ {
+   @storageRestrictions(initializes: _fetch)
+   init(initialValue) {
+     _fetch = initialValue
+   }
+   get {
+     _fetch
+   }
+   set {
+      _fetch = newValue
+   }
+ }

+ @Sendable
+ public func fetch(query p0: String) async throws -> Response {
+   try await self.fetch(p0)
+ }

+ private var _fetch: @Sendable (_ query: String) async throws -> Response = { _ in
+   XCTestDynamicOverlay.XCTFail("Unimplemented: 'fetch'")
+   throw DependenciesMacros.Unimplemented("fetch")
+ }
  ...
}

少し難しいですが、これによって2つのことを可能にしています

まずは、引数ラベル付きの関数の生成です。クロージャと違って関数は引数ラベルが使用できるので、以下のコードが可能になります。

@Dependency(\.apiClient) var apiClient
let response = try await apiClient.fetch(query: "hoge")

ただし、残念ながらこのようなコードは書けません。

@Dependency(\.apiClient.fetch) var fetch
let response = try await fetch(query: "hoge")

KeyPathは関数の指定ができなく、クロージャしか指定できないためですね。こればかりは言語仕様なのでしょうがないですね。。

2つ目は、エンドポイントのデフォルトの動作としてunimplementedを実行するクロージャを指定するコードが生成されています。
これにより、TestDependencyKeyの実装がこのようにシンプルになりました↓↓↓

extension APIClient: TestDependencyKey {
  public static let testValue = Self()
}

まとめ

このように、v1.1.0からでたマクロを使用することで、以前不満に思っていた点全てが解消されていて、より快適に開発ができそうですね。

ちなみにこのマクロが出る前にほぼ同じ機能を持ったライブラリを自作してしまい、完全にこのライブラリの役目を終えました (泣)

7
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
1