8
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

【TCA】DependencyClientマクロでボイラープレートを大幅削減

Last updated at Posted at 2023-11-23

はじめに

swift-dependenciesのv1.1.0DependencyClientというマクロが導入されました。

このマクロを使用すると、"struct of closures"スタイル1で依存クライアントを実装している場合に記述しているボイラープレートを大幅に削減することができます。

マクロ導入前のコード

Githubの検索APIをリクエストするクライアントの実装を考えます。

インタフェースは例えばこんな感じになるでしょう。

import Dependencies

public struct GithubClient: Sendable {
    public var searchRepos: @Sendable (SearchReposParams) async throws -> SearchReposResponse

    public init(searchRepos: @Sendable @escaping (SearchReposParams) async throws -> SearchReposResponse) {
        self.searchRepos = searchRepos
    }

    public struct SearchReposParams: Equatable {
        public let query: String
        public let page: Int

        public init(
            query: String,
            page: Int
        ) {
            self.query = query
            self.page = page
        }
    }
}

extension GithubClient: TestDependencyKey {
    public static let testValue: GithubClient = .init(
        searchRepos: unimplemented("\(Self.self).searchRepos")
    )

    public static let previewValue: GithubClient = .init(
        searchRepos: unimplemented("\(Self.self).searchRepos")
    )
}

public extension DependencyValues {
    var githubClient: GithubClient {
        get { self[GithubClient.self] }
        set { self[GithubClient.self] = newValue }
    }
}

Reducerでは以下のように呼び出します。

@Dependency(\.githubClient) var githubClient

.run { [query = state.query, page = state.currentPage] send in
    await send(.searchReposResponse(Result {
        try await githubClient.searchRepos(.init(query: query, page: page))
    }))
}

searchReposのクロージャの引数をあえてSearchReposParamsという構造体にしているのは、上記のようにラベル付き引数を使いたいためです。

関数型はラベル付き引数を持つことが出来ないので、構造体にしないとすると(_ query: String, _ page: Int) async throws -> SearchReposResponseという型になります。
こうすると呼び出し側のコードは以下のようになりますが、ラベルがある方が実装時も迷わないし可読性が高くなります。また、同じ型の引数が複数並ぶ場合に順序を間違えてしまうリスクもあります。
このため、自分が関わっているプロジェクトでは引数を構造体で定義するようにしています。

try await githubClient.searchRepos(query, page)

DependencyClientマクロを導入する

DependencyClientマクロを導入することで、上記のコードがどう変わるか見ていきましょう。

なお、DependencyClientマクロはDependenciesとは別のモジュールに入っているので、使用する場合はDependenciesMacrosを追加しましょう。

.target(
    name: "GithubClient",
    dependencies: [
        .product(name: "Dependencies", package: "swift-dependencies"),
        .product(name: "DependenciesMacros", package: "swift-dependencies")  // 追加
    ]
)    

モジュールを追加したら、インタフェースとなる構造体に@DependencyClientをつけます。

import Dependencies
+ import DependenciesMacros

+ @DependencyClient
public struct GithubClient: Sendable {
    // ...
}

DependencyClientマクロは以下3つの機能を提供します。

  1. 各クロージャエンドポイントの"unimplemented"実装を追加してくれる
  2. 各クロージャエンドポイントをラベル付き引数を持つメソッドに変換してくれる
  3. publicイニシャライザを追加してくれる

各クロージャエンドポイントの"unimplemented"実装を追加してくれる

すなわちtestValueプロパティのデフォルト実装を追加してくれるということです。
以下のようにSelf()を代入するコードを実装すると、マクロが"unimplemented"実装を追加してくれます。

エンドポイントが大量にある場合、コード量が一気に減りますね。

extension GithubClient: TestDependencyKey {
-   public static let testValue: GithubClient = .init(
-       searchRepos: unimplemented("\(Self.self).searchRepos")
-   )
+   public static let testValue = Self()


-   public static let previewValue: GithubClient = .init(
-       searchRepos: unimplemented("\(Self.self).searchRepos")
-   )
+   public static let previewValue = Self()
}

各クロージャエンドポイントをラベル付き引数を持つメソッドに変換してくれる

関数型はラベル付き引数を持つことが出来ないことについて前述しましたが、まさにこの問題を解決してくれます。

エンドポイントの型を以下のように定義します。

public var searchRepos: @Sendable (_ query: String, _ page: Int) async throws -> SearchReposResponse

すると以下のように呼び出すことができます。

try await githubClient.searchRepos(query: query, page: page)

引数として構造体を持たせる形だと.initを書く必要があって冗長だったんですが、マクロを使うとベストな形になりますね。

publicイニシャライザを追加してくれる

クライアントのインタフェースとライブ実装を別々のモジュールに分けている場合、インタフェースにpublicイニシャライザを実装する必要があります。

マクロを使うとなんとこの実装をごっそり消すことが出来ます。
エンドポイントが多いとこちらもメンテするのは地味に大変だったので、とても嬉しいですね。

public struct GithubClient: Sendable {
    public var searchRepos: @Sendable (SearchReposParams) async throws -> SearchReposResponse

-   public init(searchRepos: @Sendable @escaping (SearchReposParams) async throws -> SearchReposResponse) {
-       self.searchRepos = searchRepos
-   }

終わりに

DependencyClientマクロを活用することでボイラープレートを大幅に削減することができるようになりました。

DependencyClientマクロを使ったTCAのサンプルアプリを公開しているので、よかったら合わせて参考にしてください。

TCA本体にもReducerマクロが追加されたりと、TCAの世界にもマクロがどんどん進出していますね。

マクロを活用してコードをどんどんスマートにしていきましょう。

  1. TCAやってる人にはお馴染みの、ネットワーク通信等を行うクライアントを構造体で実装する設計パターンを指しています。PRの概要欄にそう書いてあったので引用しました。これが定着すると伝えやすいなあ。

8
2
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
8
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?