レイヤーの管理
前のセクションでは、実行のために提供される必要があるサービスに依存するエフェクトを作成し、そのサービスをエフェクトに提供する方法について学びました。
しかし、私たちのエフェクトプログラムの中に、構築のために他のサービスに依存しているサービスがある場合はどうでしょうか?これらの実装の詳細をサービスインターフェースに漏らさないようにしたいです。
プログラムの「依存関係グラフ」を表現し、これらの依存関係をより効果的に管理するために、Layerという強力な抽象概念を利用することができます。
レイヤーは、サービスを構築するためのコンストラクタとして機能し、サービスレベルではなく構築中に依存関係を管理できるようにします。このアプローチは、サービスインターフェースをクリーンで焦点を絞ったものに保つのに役立ちます。
| 概念 | 説明 |
|---|---|
| サービス | 特定の機能を提供する再利用可能なコンポーネントで、アプリケーションのさまざまな部分で使用されます。 |
| タグ | サービスを表すユニークな識別子で、Effect がそれを見つけて使用できるようにします。 |
| コンテキスト | タグをキー、サービスを値として機能する、サービスを格納するコレクションのことです。 |
| レイヤー | サービスを構築するための抽象概念で、サービスレベルではなく構築中に依存関係を管理します。 |
このガイドでは、以下のトピックについて説明します:
- サービスの構築を制御するためにレイヤーを使用する。
- レイヤーを使って依存関係グラフを構築する。
- エフェクトにレイヤーを提供する。
依存関係グラフの設計
ウェブアプリケーションを構築していると想像してみましょう。設定管理、ロギング、データベースアクセスを管理するアプリケーションの依存関係グラフは次のようになります。
Configサービスはアプリケーションの設定を提供します。LoggerサービスはConfigサービスに依存します。DatabaseサービスはConfigとLoggerサービスの両方に依存します。
私たちの目標は、Databaseサービスとその直接および間接の依存関係を構築することです。これは、LoggerとDatabaseの両方がConfigサービスを利用できることを保証し、これらの依存関係をDatabaseサービスに提供する必要があります。
さて、依存関係グラフをコードに変換してみましょう。
レイヤーの作成
私たちは、Services の管理ガイドで行ったように、サービスの実装を直接提供する代わりに、Databaseサービスを構築するためにレイヤーを使用します。レイヤーは、実装の詳細をサービス自体から分離する方法です。
Layer<RequirementsOut, Error, RequirementsIn>は、RequirementsOutを構築するための設計図を表します。これは、タイプRequirementsInの値を入力として受け取り、構築プロセス中にエラータイプErrorを生成する可能性があります。
私たちの場合、RequirementsOut型は構築したいサービスを表し、RequirementsInは構築に必要な依存関係を表します。
簡単のために、値の構築中にエラーが発生しないと仮定しましょう(つまり、Error = never)。
では、依存関係グラフを実装するために必要なレイヤーの数を確認しましょう:
| レイヤー | 依存関係 | タイプ |
|---|---|---|
ConfigLive | Configサービスは他のサービスに依存しない | Layer<Config> |
LoggerLive | LoggerサービスはConfigサービスに依存する | Layer<Logger, never, Config> |
DatabaseLive | DatabaseサービスはConfigとLoggerに依存する | Layer<Database, never, Config | Logger> |
サービスに複数の依存関係がある場合、それらはユニオンタイプとして表現されます。私たちの例では、DatabaseサービスはConfigとLoggerサービスの両方に依存します。したがって、DatabaseLiveレイヤーのタイプはLayer<Database, never, Config | Logger>になります。
Config
Configサービスは他のサービスに依存しないため、ConfigLiveは最も単純なレイヤーになります。Services の管理ガイドで行ったように、サービスのためのTagを作成する必要があります。そして、サービスに依存関係がないため、Layer.succeedを使用してレイヤーを直接作成できます。
import { Effect, Context, Layer } from "effect";
// Configサービスのためのタグを作成class Config extends Context.Tag("Config")< Config, { readonly getConfig: Effect.Effect<{ readonly logLevel: string; readonly connection: string; }>; }>() {}
const ConfigLive = Layer.succeed( Config, Config.of({ getConfig: Effect.succeed({ logLevel: "INFO", connection: "mysql://username:password@hostname:port/database_name", }), }));ConfigLiveのタイプを見ると、次のようなことがわかります:
RequirementsOutはConfigで、レイヤーの構築がConfigサービスを生成することを示していますErrorはneverで、レイヤー構築が失敗することがないことを示していますRequirementsInはneverで、レイヤーが依存関係を持たないことを示しています
注意すべきことは、ConfigLiveを構築するために、Config.ofコンストラクターを使用したことです。しかし、これは実装に対する正しい型推論を保証するためのヘルパーに過ぎません。このヘルパーをスキップして、単純なオブジェクトとして直接実装を構築することも可能です:
import { Effect, Context, Layer } from "effect";
class Config extends Context.Tag("Config")< Config, { readonly getConfig: Effect.Effect<{ readonly logLevel: string; readonly connection: string; }>; }>() {}
// ---cut---const ConfigLive = Layer.succeed(Config, { getConfig: Effect.succeed({ logLevel: "INFO", connection: "mysql://username:password@hostname:port/database_name", }),});Logger
次に、設定を取得するためにConfigサービスに依存するLoggerサービスの実装に進むことができます。
Services の管理ガイドで行ったように、Configタグをマッピングして、コンテキストからサービスを「抽出」できます。
Configタグを使用することは効果的な操作であるため、結果のEffectからLayerを作成するためにLayer.effectを使用します。
import { Effect, Context, Layer } from "effect";
class Config extends Context.Tag("Config")< Config, { readonly getConfig: Effect.Effect<{ readonly logLevel: string; readonly connection: string; }>; }>() {}
// ---cut---class Logger extends Context.Tag("Logger")< Logger, { readonly log: (message: string) => Effect.Effect<void> }>() {}
const LoggerLive = Layer.effect( Logger, Effect.gen(function* () { const config = yield* Config; return { log: (message) => Effect.gen(function* () { const { logLevel } = yield* config.getConfig; console.log(`[${logLevel}] ${message}`); }), }; }));LoggerLiveのタイプを見ると、以下のことがわかります:
RequirementsOutはLoggerErrorはneverで、レイヤー構築が失敗することがないことを示していますRequirementsInはConfigで、レイヤーに要件があることを示しています
Database
最後に、ConfigとLoggerサービスを使用して、Databaseサービスを実装できます。
import { Effect, Context, Layer } from "effect";
class Config extends Context.Tag("Config")< Config, { readonly getConfig: Effect.Effect<{ readonly logLevel: string; readonly connection: string; }>; }>() {}
class Logger extends Context.Tag("Logger")< Logger, { readonly log: (message: string) => Effect.Effect<void> }>() {}
// ---cut---class Database extends Context.Tag("Database")< Database, { readonly query: (sql: string) => Effect.Effect<unknown> }>() {}
const DatabaseLive = Layer.effect( Database, Effect.gen(function* () { const config = yield* Config; const logger = yield* Logger; return { query: (sql: string) => Effect.gen(function* () { yield* logger.log(`Executing query: ${sql}`); const { connection } = yield* config.getConfig; return { result: `Results from ${connection}` }; }), }; }));DatabaseLiveのタイプを見ると、RequirementsIn型がConfig | Loggerであることがわかります。つまり、DatabaseサービスはConfigとLoggerサービスの両方を必要とします。
レイヤーの組み合わせ
レイヤーは、主にマージと合成の 2 つの方法で組み合わせることができます。
レイヤーのマージ
レイヤーは、Layer.mergeコンビネータを使用してマージすることで組み合わされます:
Layer.merge(layer1, layer2);2 つのレイヤーをマージすると、結果のレイヤーは次のようになります:
- 両方のレイヤーが必要とする全てのサービスを必要とする。
- 両方のレイヤーが生成する全てのサービスを生成する。
たとえば、上記のウェブアプリケーションにおいて、ConfigLiveとLoggerLiveレイヤーを 1 つのAppConfigLiveレイヤーにマージできます。このレイヤーは、両方のレイヤーの要件(never | Config = Config)と出力(Config | Logger)を保持します:
import { Effect, Context, Layer } from "effect";
class Config extends Context.Tag("Config")< Config, { readonly getConfig: Effect.Effect<{ readonly logLevel: string; readonly connection: string; }>; }>() {}
const ConfigLive = Layer.succeed( Config, Config.of({ getConfig: Effect.succeed({ logLevel: "INFO", connection: "mysql://username:password@hostname:port/database_name", }), }));
class Logger extends Context.Tag("Logger")< Logger, { readonly log: (message: string) => Effect.Effect<void> }>() {}
const LoggerLive = Layer.effect( Logger, Effect.gen(function* () { const config = yield* Config; return { log: (message) => Effect.gen(function* () { const { logLevel } = yield* config.getConfig; console.log(`[${logLevel}] ${message}`); }), }; }));
// ---cut---const AppConfigLive = Layer.merge(ConfigLive, LoggerLive);レイヤーの合成
レイヤーはLayer.provide関数を使用して合成できます:
import { Layer } from "effect";
declare const inner: Layer.Layer<"OutInner", never, "InInner">;declare const outer: Layer.Layer<"InInner", never, "InOuter">;
const composition = inner.pipe(Layer.provide(outer));レイヤーの順次合成は、1 つのレイヤー(outer)の出力が内部レイヤー(inner)の入力として供給されることを意味し、1 つ目のレイヤーの要件と 2 つ目の出力を持つ単一のレイヤーが生成されます。
次に、AppConfigLiveレイヤーをDatabaseLiveレイヤーと合成できます:
import { Effect, Context, Layer } from "effect";
class Config extends Context.Tag("Config")< Config, { readonly getConfig: Effect.Effect<{ readonly logLevel: string; readonly connection: string; }>; }>() {}
const ConfigLive = Layer.succeed( Config, Config.of({ getConfig: Effect.succeed({ logLevel: "INFO", connection: "mysql://username:password@hostname:port/database_name", }), }));
class Logger extends Context.Tag("Logger")< Logger, { readonly log: (message: string) => Effect.Effect<void> }>() {}
const LoggerLive = Layer.effect( Logger, Effect.gen(function* () { const config = yield* Config; return { log: (message) => Effect.gen(function* () { const { logLevel } = yield* config.getConfig; console.log(`[${logLevel}] ${message}`); }), }; }));
class Database extends Context.Tag("Database")< Database, { readonly query: (sql: string) => Effect.Effect<unknown> }>() {}
const DatabaseLive = Layer.effect( Database, Effect.gen(function* () { const config = yield* Config; const logger = yield* Logger; return { query: (sql: string) => Effect.gen(function* () { yield* logger.log(`Executing query: ${sql}`); const { connection } = yield* config.getConfig; return { result: `Results from ${connection}` }; }), }; }));
// ---cut---const AppConfigLive = Layer.merge(ConfigLive, LoggerLive);
const MainLive = DatabaseLive.pipe( // データベースに設定とロガーを提供します Layer.provide(AppConfigLive), // AppConfigLiveに設定を提供します Layer.provide(ConfigLive));レイヤーのマージと合成
MainLiveレイヤーがConfigとDatabaseサービスの両方を返すようにしたいとしましょう。Layer.provideMergeを使ってこれを実現できます:
import { Effect, Context, Layer } from "effect";
class Config extends Context.Tag("Config")< Config, { readonly getConfig: Effect.Effect<{ readonly logLevel: string; readonly connection: string; }>; }>() {}
const ConfigLive = Layer.succeed( Config, Config.of({ getConfig: Effect.succeed({ logLevel: "INFO", connection: "mysql://username:password@hostname:port/database_name", }), }));
class Logger extends Context.Tag("Logger")< Logger, { readonly log: (message: string) => Effect.Effect<void> }>() {}
const LoggerLive = Layer.effect( Logger, Effect.gen(function* () { const config = yield* Config; return { log: (message) => Effect.gen(function* () { const { logLevel } = yield* config.getConfig; console.log(`[${logLevel}] ${message}`); }), }; }));
class Database extends Context.Tag("Database")< Database, { readonly query: (sql: string) => Effect.Effect<unknown> }>() {}
const DatabaseLive = Layer.effect( Database, Effect.gen(function* () { const config = yield* Config; const logger = yield* Logger; return { query: (sql: string) => Effect.gen(function* () { yield* logger.log(`Executing query: ${sql}`); const { connection } = yield* config.getConfig; return { result: `Results from ${connection}` }; }), }; }));
// ---cut---const AppConfigLive = Layer.merge(ConfigLive, LoggerLive);
const MainLive = DatabaseLive.pipe( Layer.provide(AppConfigLive), Layer.provideMerge(ConfigLive));エフェクトにレイヤーを提供する
完全に解決されたMainLiveを組み立てたので、プログラムの要件を満たすためにそれをプログラムに提供できます(Effect.provideを使用):
import { Effect, Context, Layer } from "effect";
class Config extends Context.Tag("Config")< Config, { readonly getConfig: Effect.Effect<{ readonly logLevel: string; readonly connection: string; }>; }>() {}
const ConfigLive = Layer.succeed( Config, Config.of({ getConfig: Effect.succeed({ logLevel: "INFO", connection: "mysql://username:password@hostname:port/database_name", }), }));
class Logger extends Context.Tag("Logger")< Logger, { readonly log: (message: string) => Effect.Effect<void> }>() {}
const LoggerLive = Layer.effect( Logger, Effect.gen(function* () { const config = yield* Config; return { log: (message) => Effect.gen(function* () { const { logLevel } = yield* config.getConfig; console.log(`[${logLevel}] ${message}`); }), }; }));
class Database extends Context.Tag("Database")< Database, { readonly query: (sql: string) => Effect.Effect<unknown> }>() {}
const DatabaseLive = Layer.effect( Database, Effect.gen(function* () { const config = yield* Config; const logger = yield* Logger; return { query: (sql: string) => Effect.gen(function* () { yield* logger.log(`Executing query: ${sql}`); const { connection } = yield* config.getConfig; return { result: `Results from ${connection}` }; }), }; }));
const AppConfigLive = Layer.merge(ConfigLive, LoggerLive);
const MainLive = DatabaseLive.pipe( Layer.provide(AppConfigLive), Layer.provide(ConfigLive));
// ---cut---const program = Effect.gen(function* () { const database = yield* Database; const result = yield* database.query("SELECT * FROM users"); return yield* Effect.succeed(result);});
const runnable = Effect.provide(program, MainLive);
Effect.runPromise(runnable).then(console.log);/*出力:[INFO] Executing query: SELECT * FROM users{ result: 'Results from mysql://username:password@hostname:port/database_name'}*/