はじめに
swift-dependenciesのv1.1.0でDependencyClient
というマクロが導入されました。
このマクロを使用すると、"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つの機能を提供します。
- 各クロージャエンドポイントの
"unimplemented"
実装を追加してくれる - 各クロージャエンドポイントをラベル付き引数を持つメソッドに変換してくれる
- 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の世界にもマクロがどんどん進出していますね。
マクロを活用してコードをどんどんスマートにしていきましょう。
-
TCAやってる人にはお馴染みの、ネットワーク通信等を行うクライアントを構造体で実装する設計パターンを指しています。PRの概要欄にそう書いてあったので引用しました。これが定着すると伝えやすいなあ。 ↩