💉

EffectのContextを使って疎結合なコードを書く

2024/02/22に公開

アプリケーションの開発において、複雑な依存関係や異なるコンポーネント間の強い結びつきはテストの難しさやコードの保守性の低下を引き起こす主な原因の一つです。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>を提供することによって動かすことができます。

サービスを定義する

typeinterfaceを使ってサービスを定義します。ここでは例として外部のデータソースから特定のユーザ情報を取得するサービスを定義します。

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