EffectのContextを使って疎結合なコードを書く
アプリケーションの開発において、複雑な依存関係や異なるコンポーネント間の強い結びつきはテストの難しさやコードの保守性の低下を引き起こす主な原因の一つです。EffectライブラリのContextという仕組みは、コンポーネント間を疎結合に保ちつつ、ポリモーフィズムを利用した柔軟なコード設計を可能にし課題を解決する助けとなります。本記事ではそんなContextの基本的な使い方を紹介します。
実行環境
- Node.js v20.11.1
- effect 2.4.0
- TypeScript 5.3,3
用語
サービス
Effectライブラリではモジュールの振る舞いを表すインタフェースをサービスと呼びます。SOLID原則といった代表的な設計原則に基づいてサービスを分割することにより、テストとメンテナンスが容易なコードを書くことができます。
Context
Contextはサービスの実装を管理するためのコンテナオブジェクトです。Effect型はEffect<A, E, R>
と表現されますが、型パラメータR
はこのEffectが要求するサービスを表しています。何かしらのサービスR
に依存するEffectは、そのサービスの実装を管理するContext<R>
を提供することによって動かすことができます。
サービスを定義する
type
やinterface
を使ってサービスを定義します。ここでは例として外部のデータソースから特定のユーザ情報を取得するサービスを定義します。
import { Effect } from 'effect'
type User = {
id: string
name: string
}
type FetchUser = (id: string) => Effect.Effect<User, Error>
Contextを作成する
次にサービスの実装と紐づくタグオブジェクトを作成します。
import { Context } from 'effect'
const FetchUser = Context.GenericTag<FetchUser>('FetchUser')
Context
はサービスの実装を保持するコンテナです。その実体はタグオブジェクトをkey、サービスの実装をvalueとしたMap
オブジェクトです。したがって、Context
モジュールのmake
関数にタグオブジェクトとサービス実装を渡すことによりContext
オブジェクトを作ることができます。
import { Context } from 'effect'
const FetchUserContext: Context.Context<FetchUser> = Context.make(
FetchUser,
(id: string) => {
// 本来はREST APIやDBからユーザ情報を取得することが多いと思いますが、
// この記事の本質とは関係ないので簡易な実装にしています。
const user: User = { id, name: 'Alice' }
return Effect.succeed(user)
})
サービスを利用するEffectを実装する
FetchUser
を利用するEffectを作成する。
import { Effect } from 'effect'
const program: Effect.Effect<void, Error, FetchUser> = Effect.gen(function* (_) {
// タグオブジェクトを介してContextからサービスの実装を取り出す
const fetchUserService = yield* _(FetchUser)
// ユーザ情報を取得する
const user = yield* _(fetchUserService('USER000001'))
yield* _(Console.log(`ID: ${user.id}, Name: ${user.name}`))
})
const fetchUserService = yield* _(FetchUser)
が最も重要なところです。タグオブジェクトを介してFetchUser
サービスにアクセスしているため、このEffectはFetchUser
サービスの実装についての知識は持っていません。すなわちこのEffectからはFetchUser
サービスの実装が隠蔽されています。
また、Effectの型がEffect<void, Error, FecthUser>
となっており、このEffectがFetchUser
に依存していることもTypeScriptに推論されています。(コード上は明示しています)
EffectにContextを注入する
このEffectを動かそうとするとエラーが発生します。
import { Effect } from 'effect'
Effect.runFork(program)
// Error: Service not found: Symbol(FetchUser)
エラーメッセージにある通り、このEffectが依存しているFetchUser
サービスの依存関係が解決できないためです。このEffectに先に作成したContext
を注入することによって動かせるようになります。
import { Effect } from 'effect'
// 第三型パラメータがneverになっているため、依存関係が解決されているおとがわかる
const runnable: Effect.Effect<void, Error, never> = Effect.provide(program, FetchUserContext)
Effect.runFork(runnable)
// > ID: USER000001, Name: Alice
終わりに
Context
を使って特定のサービスに依存するEffectに依存関係を注入する方法を説明しました。テストのためにモック実装を注入するといったテクニックをEffectでも利用することができます。
Context
の作り方やEffectへの注入の仕方はこの記事で説明した以外の方法にもいくつか存在します。それについてはまた別の機会にでも紹介したいと思います。
Discussion