管理 Layer
学习如何使用 Layer 管理服务依赖,为应用构建高效、清晰的依赖图。
在管理 Service页面中,你学习了如何创建依赖某个 Service 才能执行的 Effect,以及如何为该 Effect 提供这个 Service。
然而,如果 Effect 程序中的某个 Service 自身在构建时依赖其他 Service,该怎么办?我们希望避免把这些实现细节泄漏到 Service 接口中。
为了表示程序的“依赖图”并更有效地管理这些依赖,我们可以使用一个强大的抽象,称为 “Layer”。
Layer 充当创建 Service 的构造器,让我们能够在构造期间而非 Service 层面管理依赖。这种方式有助于保持 Service 接口的简洁与专注。
在深入细节之前,让我们先回顾一些关键概念:
| 概念 | 描述 |
|---|---|
| service | 可复用的组件,提供特定功能,在应用的不同部分被使用。 |
| service key | 表示 service 的唯一标识符,让 Effect 能够定位并使用它。 |
| context | Service 的集合,类似一个以 service key 为键、service 为值的 map。 |
| layer | 用于构建 service 的抽象,在构造期间而非 Service 层面管理依赖。 |
设计依赖图
假设我们正在构建一个 Web 应用。可以想象,对于需要管理配置、日志和数据库访问的应用,其依赖图大致如下:
Configservice 提供应用配置。Loggerservice 依赖Configservice。Databaseservice 同时依赖Config和Loggerservice。
我们的目标是构建 Database service 及其直接与间接依赖。这意味着需要确保 Config service 对 Logger 和 Database 都可用,然后把这些依赖提供给 Database service。
避免需求泄漏
在构造 Database service 时,重要的是避免在 Database 接口中暴露对 Config 和 Logger 的依赖。
你可能会想按如下方式定义 Database service:
示例(在 Service 接口中泄漏依赖)
import { Effect, Context } from "effect"
// Declaring a service key for the Config service
class Config extends Context.Service<Config, {}>()("Config") {}
// Declaring a service key for the Logger service
class Logger extends Context.Service<Logger, {}>()("Logger") {}
// Declaring a service key for the Database service
class Database extends Context.Service<
Database,
{
// ❌ Avoid exposing Config and Logger as a requirement
readonly query: (
sql: string,
) => Effect.Effect<unknown, never, Config | Logger>
}
>()("Database") {}
Database.key // => "Database"
这里,Database service 的 query 函数同时需要 Config 和 Logger。这种设计泄漏了实现细节,使 Database service 意识到自己的依赖,从而让测试变得复杂、难以 mock。
Service 函数应避免直接声明依赖。在实践中,service 操作的 Requirements 参数应设置为 never:
┌─── No dependencies required
▼
Effect<Success, Error, never>为演示这一问题,我们来创建一个 Database service 的测试实例:
示例(创建带有泄漏依赖的测试实例)
import { Effect, Context } from "effect"
// Declaring a service key for the Config service
class Config extends Context.Service<Config, {}>()("Config") {}
// Declaring a service key for the Logger service
class Logger extends Context.Service<Logger, {}>()("Logger") {}
// Declaring a service key for the Database service
class Database extends Context.Service<
Database,
{
readonly query: (
sql: string,
) => Effect.Effect<unknown, never, Config | Logger>
}
>()("Database") {}
// Declaring a test instance of the Database service
const DatabaseTest = Database.of({
// Simulating a simple response
query: (sql: string) => Effect.succeed([]),
})
import * as assert from "node:assert"
// A test that uses the Database service
const test = Effect.gen(function* () {
const database = yield* Database
const result = yield* database.query("SELECT * FROM users")
assert.deepStrictEqual(result, [])
})
// ┌─── Effect<unknown, never, Config | Logger>
// ▼
const incompleteTestSetup = test.pipe(
// Attempt to provide only the Database service without Config and Logger
Effect.provideService(Database, DatabaseTest),
)
Database.key // => "Database"
由于 Database service 接口直接包含对 Config 和 Logger 的依赖,任何测试准备工作都被迫包含这些 service,即使它们与测试无关。这带来了不必要的复杂度,也让编写简单、隔离的单元测试变得困难。
与其把依赖直接绑定到 Database service 接口上,不如在构造阶段管理依赖。
我们可以使用 Layer 来正确构造 Database service 并管理其依赖,而不会把细节泄漏到接口中。
当 service 自身带有需求时,最好把实现细节分离到 Layer 中。Layer 充当创建该 service 的构造器,让我们能够在构造层面而非 service 层面处理依赖。
创建 Layer
Layer 类型的结构如下:
┌─── The service to be created
│ ┌─── The possible error
│ │ ┌─── The required dependencies
▼ ▼ ▼
Layer<RequirementsOut, Error, RequirementsIn>
Layer 表示构造 RequirementsOut(即 service)的蓝图。它以 RequirementsIn(依赖)作为输入,并可能在构造过程中产生 Error 类型的错误。
| 参数 | 描述 |
|---|---|
RequirementsOut | 要创建的 service 或资源。 |
Error | 构造 service 时可能发生的错误类型。 |
RequirementsIn | 构造 service 所需的依赖。 |
通过使用 Layer,你可以更好地组织 service,确保其依赖被清晰定义,并与实现细节分离。
为简单起见,我们假设值构造过程中不会遇到任何错误(即 Error = never)。
现在,让我们确定实现依赖图需要多少个 Layer:
| Layer | 依赖 | 类型 |
|---|---|---|
ConfigLive | Config service 不依赖任何其他 service | Layer<Config> |
LoggerLive | Logger service 依赖 Config service | Layer<Logger, never, Config> |
DatabaseLive | Database service 依赖 Config 和 Logger | Layer<Database, never, Config | Logger> |
为某个 service 的 Layer 命名时,一个常见约定是:为 “live” 实现添加 Live 后缀,为 “test” 实现添加 Test 后缀。例如,对于 Database service,DatabaseLive 是你在应用中提供的 Layer,而 DatabaseTest 是你在测试中提供的 Layer。
当一个 service 有多个依赖时,它们表示为联合类型。在我们的例子中,Database service 同时依赖 Config 和 Logger service。因此,DatabaseLive Layer 的类型为:
Layer<Database, never, Config | Logger>
Config
Config service 不依赖任何其他 service,因此 ConfigLive 是最容易实现的 Layer。正如管理 Service页面中那样,我们必须为该 service 创建一个 service key。由于该 service 没有依赖,我们可以直接使用 Layer.succeed 构造器创建 Layer:
import { Effect, Context, Layer } from "effect"
// Declaring a service key for the Config service
class Config extends Context.Service<
Config,
{
readonly getConfig: Effect.Effect<{
readonly logLevel: string
readonly connection: string
}>
}
>()("Config") {}
// Layer<Config, never, never>
const ConfigLive = Layer.succeed(
Config,
Config.of({
getConfig: Effect.succeed({
logLevel: "INFO",
connection: "mysql://username:password@hostname:port/database_name",
}),
}),
)
await Effect.runPromise(
Effect.provide(Config.pipe(Effect.andThen((c) => c.getConfig)), ConfigLive),
) // => { logLevel: "INFO", connection: "mysql://username:password@hostname:port/database_name" }
观察 ConfigLive 的类型,我们可以发现:
RequirementsOut是Config,表明构造该 Layer 将产出ConfigserviceError是never,表明 Layer 构造不会失败RequirementsIn是never,表明该 Layer 没有依赖
注意,为了构造 ConfigLive,我们使用了 Config.of 构造器。然而,这只是一个用于确保实现具有正确类型推断的辅助方法。也可以跳过这个辅助方法,直接把实现构造成一个简单对象:
import { Effect, Context, Layer } from "effect"
// Declaring a service key for the Config service
class Config extends Context.Service<
Config,
{
readonly getConfig: Effect.Effect<{
readonly logLevel: string
readonly connection: string
}>
}
>()("Config") {}
// Layer<Config, never, never>
const ConfigLive = Layer.succeed(Config, {
getConfig: Effect.succeed({
logLevel: "INFO",
connection: "mysql://username:password@hostname:port/database_name",
}),
})
await Effect.runPromise(
Effect.provide(Config.pipe(Effect.andThen((c) => c.getConfig)), ConfigLive),
) // => { logLevel: "INFO", connection: "mysql://username:password@hostname:port/database_name" }
Logger
现在我们继续实现 Logger service,它依赖 Config service 来获取一些配置。
正如我们在管理 Service页面中所做的那样,我们可以 yield Config service key,从 Context 中“提取”该 service。
由于使用 Config service key 是一个带 Effect 的操作,我们使用 Layer.effect 从生成的 Effect 创建 Layer。
import { Effect, Context, Layer } from "effect"
// Declaring a service key for the Config service
class Config extends Context.Service<
Config,
{
readonly getConfig: Effect.Effect<{
readonly logLevel: string
readonly connection: string
}>
}
>()("Config") {}
// Layer<Config, never, never>
const ConfigLive = Layer.succeed(Config, {
getConfig: Effect.succeed({
logLevel: "INFO",
connection: "mysql://username:password@hostname:port/database_name",
}),
})
// Declaring a service key for the Logger service
class Logger extends Context.Service<
Logger,
{ readonly log: (message: string) => Effect.Effect<void> }
>()("Logger") {}
// Layer<Logger, never, Config>
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}`)
}),
}
}),
)
await Effect.runPromise(
Effect.provide(
Logger.pipe(Effect.andThen((logger) => logger.log("hello"))),
Layer.provide(LoggerLive, ConfigLive),
),
) // => undefined
观察 LoggerLive 的类型:
Layer<Logger, never, Config>
我们可以发现:
RequirementsOut是LoggerError是never,表明 Layer 构造不会失败RequirementsIn是Config,表明该 Layer 有一个需求
Database
最后,我们可以使用 Config 和 Logger service 来实现 Database service。
import { Effect, Context, Layer } from "effect"
// Declaring a service key for the Config service
class Config extends Context.Service<
Config,
{
readonly getConfig: Effect.Effect<{
readonly logLevel: string
readonly connection: string
}>
}
>()("Config") {}
// Layer<Config, never, never>
const ConfigLive = Layer.succeed(Config, {
getConfig: Effect.succeed({
logLevel: "INFO",
connection: "mysql://username:password@hostname:port/database_name",
}),
})
// Declaring a service key for the Logger service
class Logger extends Context.Service<
Logger,
{ readonly log: (message: string) => Effect.Effect<void> }
>()("Logger") {}
// Layer<Logger, never, Config>
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}`)
}),
}
}),
)
// Declaring a service key for the Database service
class Database extends Context.Service<
Database,
{ readonly query: (sql: string) => Effect.Effect<unknown> }
>()("Database") {}
// Layer<Database, never, Config | Logger>
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).pipe(
Layer.provide(ConfigLive),
)
await Effect.runPromise(
Effect.provide(
Database.pipe(Effect.andThen((db) => db.query("SELECT * FROM users"))),
Layer.provide(DatabaseLive, AppConfigLive),
),
) // => { result: "Results from mysql://username:password@hostname:port/database_name" }
观察 DatabaseLive 的类型:
Layer<Database, never, Config | Logger>
我们可以发现 RequirementsIn 类型是 Config | Logger,也就是说 Database service 同时需要 Config 和 Logger service。
组合 Layer
Layer 可以通过两种主要方式组合:合并(merging)与组合(composing)。
合并 Layer
Layer 可以通过 Layer.merge 函数进行合并:
import { Layer } from "effect"
declare const layer1: Layer.Layer<"Out1", never, "In1">
declare const layer2: Layer.Layer<"Out2", never, "In2">
// Layer<"Out1" | "Out2", never, "In1" | "In2">
const merging = Layer.merge(layer1, layer2)
当我们合并两个 Layer 时,得到的 Layer:
- 需要它们两者所需的所有 service(
"In1" | "In2")。 - 产出它们两者产出的所有 service(
"Out1" | "Out2")。
例如,在上面的 Web 应用中,我们可以把 ConfigLive 和 LoggerLive 合并成单个 AppConfigLive Layer,它保留两个 Layer 的需求(never | Config = Config)以及两个 Layer 的输出(Config | Logger):
import { Effect, Context, Layer } from "effect"
// Declaring a service key for the Config service
class Config extends Context.Service<
Config,
{
readonly getConfig: Effect.Effect<{
readonly logLevel: string
readonly connection: string
}>
}
>()("Config") {}
// Layer<Config, never, never>
const ConfigLive = Layer.succeed(Config, {
getConfig: Effect.succeed({
logLevel: "INFO",
connection: "mysql://username:password@hostname:port/database_name",
}),
})
// Declaring a service key for the Logger service
class Logger extends Context.Service<
Logger,
{ readonly log: (message: string) => Effect.Effect<void> }
>()("Logger") {}
// Layer<Logger, never, Config>
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}`)
}),
}
}),
)
// Layer<Config | Logger, never, Config>
const AppConfigLive = Layer.merge(ConfigLive, LoggerLive)
await Effect.runPromise(
Effect.provide(
Config.pipe(Effect.andThen((c) => c.getConfig)),
AppConfigLive.pipe(Layer.provide(ConfigLive)),
),
) // => { logLevel: "INFO", connection: "mysql://username:password@hostname:port/database_name" }
组合 Layer
Layer 可以使用 Layer.provide 函数进行组合:
import { Layer } from "effect"
declare const inner: Layer.Layer<"OutInner", never, "InInner">
declare const outer: Layer.Layer<"InInner", never, "InOuter">
// Layer<"OutInner", never, "InOuter">
const composition = Layer.provide(inner, outer)
Layer 的顺序组合意味着一个 Layer 的输出被作为内层 Layer 的输入提供,结果得到单个 Layer:它拥有外层 Layer 的需求和内层 Layer 的输出。
现在我们可以把 AppConfigLive Layer 与 DatabaseLive Layer 组合起来:
import { Effect, Context, Layer } from "effect"
// Declaring a service key for the Config service
class Config extends Context.Service<
Config,
{
readonly getConfig: Effect.Effect<{
readonly logLevel: string
readonly connection: string
}>
}
>()("Config") {}
// Layer<Config, never, never>
const ConfigLive = Layer.succeed(Config, {
getConfig: Effect.succeed({
logLevel: "INFO",
connection: "mysql://username:password@hostname:port/database_name",
}),
})
// Declaring a service key for the Logger service
class Logger extends Context.Service<
Logger,
{ readonly log: (message: string) => Effect.Effect<void> }
>()("Logger") {}
// Layer<Logger, never, Config>
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}`)
}),
}
}),
)
// Declaring a service key for the Database service
class Database extends Context.Service<
Database,
{ readonly query: (sql: string) => Effect.Effect<unknown> }
>()("Database") {}
// Layer<Database, never, Config | Logger>
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}` }
}),
}
}),
)
// Layer<Config | Logger, never, Config>
const AppConfigLive = Layer.merge(ConfigLive, LoggerLive)
// Layer<Database, never, never>
const MainLive = DatabaseLive.pipe(
// provides the config and logger to the database
Layer.provide(AppConfigLive),
// provides the config to AppConfigLive
Layer.provide(ConfigLive),
)
await Effect.runPromise(
Effect.provide(
Database.pipe(Effect.andThen((db) => db.query("SELECT * FROM users"))),
MainLive,
),
) // => { result: "Results from mysql://username:password@hostname:port/database_name" }
我们得到了一个产出 Database service 的 MainLive Layer:
Layer<Database, never, never>
该 Layer 是我们应用完全解析后的 Layer。
合并与组合 Layer
假设我们希望 MainLive Layer 同时返回 Config 和 Database service。可以通过 Layer.provideMerge 实现:
import { Effect, Context, Layer } from "effect"
// Declaring a service key for the Config service
class Config extends Context.Service<
Config,
{
readonly getConfig: Effect.Effect<{
readonly logLevel: string
readonly connection: string
}>
}
>()("Config") {}
const ConfigLive = Layer.succeed(Config, {
getConfig: Effect.succeed({
logLevel: "INFO",
connection: "mysql://username:password@hostname:port/database_name",
}),
})
// Declaring a service key for the Logger service
class Logger extends Context.Service<
Logger,
{ readonly log: (message: string) => Effect.Effect<void> }
>()("Logger") {}
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}`)
}),
}
}),
)
// Declaring a service key for the Database service
class Database extends Context.Service<
Database,
{ readonly query: (sql: string) => Effect.Effect<unknown> }
>()("Database") {}
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}` }
}),
}
}),
)
// Layer<Config | Logger, never, Config>
const AppConfigLive = Layer.merge(ConfigLive, LoggerLive)
// Layer<Config | Database, never, never>
const MainLive = DatabaseLive.pipe(
Layer.provide(AppConfigLive),
Layer.provideMerge(ConfigLive),
)
await Effect.runPromise(
Effect.provide(
Effect.gen(function* () {
const config = yield* Config
const database = yield* Database
const queryResult = yield* database.query("SELECT * FROM users")
return { logLevel: (yield* config.getConfig).logLevel, queryResult }
}),
MainLive,
),
) // => { logLevel: "INFO", queryResult: { result: "Results from mysql://username:password@hostname:port/database_name" } }
为 Effect 提供 Layer
现在我们已经为应用组装好了完全解析的 MainLive,可以使用 Effect.provide 将它提供给程序,以满足程序的需求:
import { Effect, Context, Layer } from "effect"
class Config extends Context.Service<
Config,
{
readonly getConfig: Effect.Effect<{
readonly logLevel: string
readonly connection: string
}>
}
>()("Config") {}
const ConfigLive = Layer.succeed(Config, {
getConfig: Effect.succeed({
logLevel: "INFO",
connection: "mysql://username:password@hostname:port/database_name",
}),
})
class Logger extends Context.Service<
Logger,
{ readonly log: (message: string) => Effect.Effect<void> }
>()("Logger") {}
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.Service<
Database,
{ readonly query: (sql: string) => Effect.Effect<unknown> }
>()("Database") {}
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),
)
// ┌─── Effect<unknown, never, Database>
// ▼
const program = Effect.gen(function* () {
const database = yield* Database
const result = yield* database.query("SELECT * FROM users")
return result
})
// ┌─── Effect<unknown, never, never>
// ▼
const runnable = Effect.provide(program, MainLive)
await Effect.runPromise(runnable) // => { result: "Results from mysql://username:password@hostname:port/database_name" }
/*
Output:
[INFO] Executing query: SELECT * FROM users
*/
注意 runnable 的需求类型是 never,表明该程序运行时不需要任何额外的 service。
把 Layer 转换为 Effect
有时你的整个应用可能就是一个 Layer,例如一个 HTTP server。你可以用 Layer.launch 把该 Layer 转换为 Effect。它会构造 Layer 并使其保持存活,直到被中断。
示例(启动一个 HTTP Server Layer)
import { Console, Context, Effect, Layer } from "effect"
class HTTPServer extends Context.Service<HTTPServer, void>()("HTTPServer") {}
// Simulating an HTTP server
const server = Layer.effect(
HTTPServer,
// Log a message to simulate a server starting
Console.log("Listening on http://localhost:3000"),
)
// Converts the layer to an effect and runs it
Effect.runFork(Layer.launch(server))
/*
Output:
Listening on http://localhost:3000
...
*/
// Layer.launch never completes on its own; building the layer directly
// lets us verify what it produces without hanging the process
const context = await Effect.runPromise(Effect.scoped(Layer.build(server)))
Context.get(context, HTTPServer) // => undefined
Tap 操作
Layer.tap 和 Layer.tapError 函数允许你根据 Layer 的成功或失败执行额外的 Effect。这些操作不会修改 Layer 的签名,但在 Layer 构造期间用于日志记录或执行副作用非常有用。
Layer.tap:当 Layer 成功获取时执行指定的 Effect。Layer.tapError:当 Layer 获取失败时执行指定的 Effect。
示例(记录 Layer 获取过程中的成功与失败)
import { Config, Context, Effect, Layer, Console } from "effect"
class HTTPServer extends Context.Service<HTTPServer, void>()("HTTPServer") {}
// Simulating an HTTP server
const server = Layer.effect(
HTTPServer,
Effect.gen(function* () {
const host = yield* Config.String("HOST")
console.log(`Listening on http://localhost:${host}`)
}),
).pipe(
// Log a message if the layer acquisition succeeds
Layer.tap((ctx) => Console.log(`layer acquisition succeeded with:\n${ctx}`)),
// Log a message if the layer acquisition fails
Layer.tapError((err) =>
Console.log(`layer acquisition failed with:\n${err}`),
),
)
Effect.runFork(Layer.launch(server))
/*
Output:
layer acquisition failed with:
(Missing data at HOST: "Expected HOST to exist in the process context")
*/
错误处理
在构造 Layer 时,处理潜在错误很重要。Layer.catch 可以检查获取错误并返回一个回退 Layer。
catch
Layer.catch 函数允许你通过指定回退 Layer 从 Layer 构造期间的错误中恢复。这对于处理特定错误情况、确保应用能以替代方案继续运行很有用。
示例(从 Layer 构造期间的错误中恢复)
import { Config, Context, Effect, Layer } from "effect"
class HTTPServer extends Context.Service<HTTPServer, void>()("HTTPServer") {}
// Simulating an HTTP server
const server = Layer.effect(
HTTPServer,
Effect.gen(function* () {
const host = yield* Config.String("HOST")
console.log(`Listening on http://localhost:${host}`)
}),
).pipe(
// Recover from errors during layer construction
Layer.catch((configError) =>
Layer.effect(
HTTPServer,
Effect.gen(function* () {
console.log(`Recovering from error:\n${configError}`)
console.log(`Listening on http://localhost:3000`)
}),
),
),
)
Effect.runFork(Layer.launch(server))
/*
Output:
Recovering from error:
(Missing data at HOST: "Expected HOST to exist in the process context")
Listening on http://localhost:3000
...
*/
如果回退逻辑不需要该错误,可以忽略传给 Layer.catch 的参数。
示例(回退到替代 Layer)
import { Config, Context, Effect, Layer } from "effect"
class Database extends Context.Service<Database, void>()("Database") {}
// Simulating a database connection
const postgresDatabaseLayer = Layer.effect(
Database,
Effect.gen(function* () {
const databaseConnectionString = yield* Config.String("CONNECTION_STRING")
console.log(`Connecting to database with: ${databaseConnectionString}`)
}),
)
// Simulating an in-memory database connection
const inMemoryDatabaseLayer = Layer.effect(
Database,
Effect.gen(function* () {
console.log(`Connecting to in-memory database`)
}),
)
// Fallback to in-memory database if PostgreSQL connection fails
const database = postgresDatabaseLayer.pipe(
Layer.catch(() => inMemoryDatabaseLayer),
)
Effect.runFork(Layer.launch(database))
/*
Output:
Connecting to in-memory database
...
*/
使用 Context.Service 定义 Service
Context.Service 可以把 service key 及其构造 Effect 一起定义为一个类。之后你可以把 Layer 作为该类的静态字段暴露出来。
定义带依赖的 Service
下面的例子定义了一个依赖文件系统的 Cache service。
示例(定义 Cache Service)
import { NodeFileSystem } from "@effect/platform-node"
import { Context, Effect, FileSystem, Layer } from "effect"
// Define a Cache service
class Cache extends Context.Service<Cache>()("app/Cache", {
// Define how to create the service
make: Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem
const lookup = (key: string) => fs.readFileString(`cache/${key}`)
return { lookup } as const
}),
}) {
// Specify dependencies
static readonly layer = Layer.effect(this, this.make).pipe(
Layer.provide(NodeFileSystem.layer),
)
}
Cache.key // => "app/Cache"
声明 Service 的 Layer
把 service 的 Layer 声明为静态类字段,通过 Layer.effect 从 this.make 构建。下面的字段名只是约定,并非 API 的一部分。
| 静态字段 | 描述 |
|---|---|
Cache.layer | 提供 Cache service,并已包含其依赖。 |
Cache.layerWithoutDependencies | 提供 Cache service,但需要单独提供依赖。 |
import { NodeFileSystem } from "@effect/platform-node"
import { Context, Effect, FileSystem, Layer } from "effect"
// Define a Cache service
class Cache extends Context.Service<Cache>()("app/Cache", {
make: Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem
const lookup = (key: string) => fs.readFileString(`cache/${key}`)
return { lookup } as const
}),
}) {
static readonly layer = Layer.effect(this, this.make).pipe(
Layer.provide(NodeFileSystem.layer),
)
static readonly layerWithoutDependencies = Layer.effect(this, this.make)
}
// Layer that includes all required dependencies
//
// ┌─── Layer<Cache>
// ▼
const layer = Cache.layer
// Layer without dependencies, requiring them to be provided externally
//
// ┌─── Layer.Layer<Cache, never, FileSystem>
// ▼
const layerNoDeps = Cache.layerWithoutDependencies
// Exercise layerNoDeps with a test FileSystem to confirm the wiring works
const FileSystemTest = FileSystem.layerNoop({
readFileString: () => Effect.succeed("File Content..."),
})
await Effect.runPromise(
Effect.provide(
Effect.gen(function* () {
const cache = yield* Cache
return yield* cache.lookup("my-key")
}),
Layer.provide(layerNoDeps, FileSystemTest),
),
) // => "File Content..."
访问 Service
使用 Context.Service 创建的 service 可以像其他任何 Effect service 一样被访问。
示例(访问 Cache Service)
import { NodeFileSystem } from "@effect/platform-node"
import { Context, Effect, FileSystem, Layer, Console } from "effect"
// Define a Cache service
class Cache extends Context.Service<Cache>()("app/Cache", {
make: Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem
const lookup = (key: string) => fs.readFileString(`cache/${key}`)
return { lookup } as const
}),
}) {
static readonly layer = Layer.effect(this, this.make).pipe(
Layer.provide(NodeFileSystem.layer),
)
static readonly layerWithoutDependencies = Layer.effect(this, this.make)
}
// Accessing the Cache Service
const program = Effect.gen(function* () {
const cache = yield* Cache
const data = yield* cache.lookup("my-key")
console.log(data)
}).pipe(Effect.catchCause((cause) => Console.log(cause)))
const runnable = program.pipe(Effect.provide(Cache.layer))
Effect.runFork(runnable)
/*
{
_id: 'Cause',
failures: [
{
_tag: 'Fail',
error: {
_tag: 'PlatformError',
reason: {
_tag: 'NotFound',
module: 'FileSystem',
method: 'readFile',
pathOrDescriptor: 'cache/my-key',
syscall: 'open',
cause: [Error: ENOENT: no such file or directory, open 'cache/my-key']
}
}
}
]
}
*/
由于该示例使用 Cache.layer,它会与真实文件系统交互。如果文件不存在,就会产生错误。
注入测试依赖
为了在不依赖真实文件系统的情况下测试程序,我们可以使用 Cache.layerWithoutDependencies Layer 注入一个测试文件系统。
示例(使用测试文件系统)
import { NodeFileSystem } from "@effect/platform-node"
import { Context, Effect, FileSystem, Layer, Console } from "effect"
// Define a Cache service
class Cache extends Context.Service<Cache>()("app/Cache", {
make: Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem
const lookup = (key: string) => fs.readFileString(`cache/${key}`)
return { lookup } as const
}),
}) {
static readonly layer = Layer.effect(this, this.make).pipe(
Layer.provide(NodeFileSystem.layer),
)
static readonly layerWithoutDependencies = Layer.effect(this, this.make)
}
// Accessing the Cache Service
const program = Effect.gen(function* () {
const cache = yield* Cache
const data = yield* cache.lookup("my-key")
console.log(data)
}).pipe(Effect.catchCause((cause) => Console.log(cause)))
// Create a test file system that always returns a fixed value
const FileSystemTest = FileSystem.layerNoop({
readFileString: () => Effect.succeed("File Content..."),
})
const runnable = program.pipe(
Effect.provide(Cache.layerWithoutDependencies),
// Provide the mock file system
Effect.provide(FileSystemTest),
)
Effect.runFork(runnable)
// Output: File Content...
await Effect.runPromise(runnable) // => undefined
直接 Mock Service
另一种方式是不替换依赖,而是直接 mock Cache service 本身。
示例(Mock Cache Service)
import { NodeFileSystem } from "@effect/platform-node"
import { Context, Effect, FileSystem, Layer, Console } from "effect"
// Define a Cache service
class Cache extends Context.Service<Cache>()("app/Cache", {
make: Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem
const lookup = (key: string) => fs.readFileString(`cache/${key}`)
return { lookup } as const
}),
}) {
static readonly layer = Layer.effect(this, this.make).pipe(
Layer.provide(NodeFileSystem.layer),
)
static readonly layerWithoutDependencies = Layer.effect(this, this.make)
}
// Accessing the Cache Service
const program = Effect.gen(function* () {
const cache = yield* Cache
const data = yield* cache.lookup("my-key")
console.log(data)
}).pipe(Effect.catchCause((cause) => Console.log(cause)))
// Create a mock implementation of Cache
const cache = Cache.of({
lookup: () => Effect.succeed("Cache Content..."),
})
// Provide the mock Cache service
const runnable = program.pipe(Effect.provideService(Cache, cache))
Effect.runFork(runnable)
// Output: Cache Content...
await Effect.runPromise(runnable) // => undefined
构造 make 的其他方式
Context.Service 的 make 字段接受任何 Effect,因此 service 可以使用不同的构造风格:
| 风格 | 如何构建 make |
|---|---|
| 静态值 | Effect.succeed(...):一个常量实现。 |
| 同步 | Effect.sync(() => ...):一个同步构造器。 |
| Effectful | Effect.gen(function* () { ... })(或任何其他 Effect):一个自身依赖其他 service 的构造器。 |
| Scoped | 在 make 内使用 Effect.acquireRelease/Effect.addFinalizer 的 Effect.gen(...):生命周期管理,无需单独的选项。 |
示例(定义具有静态实现的 Service)
这是定义 service 最简单的方式。当你希望为 service 提供一个常量值时,它很有用。
import { Context, Effect, Layer } from "effect"
class MagicNumber extends Context.Service<MagicNumber>()("MagicNumber", {
make: Effect.succeed({ value: 42 }),
}) {
static readonly layer = Layer.effect(this, this.make)
}
// ┌─── Effect<void, never, MagicNumber>
// ▼
const program = Effect.gen(function* () {
const magicNumber = yield* MagicNumber
console.log(`The magic number is ${magicNumber.value}`)
})
await Effect.runPromise(program.pipe(Effect.provide(MagicNumber.layer))) // => undefined
// The magic number is 42
示例(定义具有同步构造器的 Service)
import { Context, Effect, Layer, Random } from "effect"
class Sync extends Context.Service<Sync>()("Sync", {
make: Effect.sync(() => ({
next: Random.nextInt,
})),
}) {
static readonly layer = Layer.effect(this, this.make)
}
// ┌─── Effect<void, never, Sync>
// ▼
const program = Effect.gen(function* () {
const sync = yield* Sync
const n = yield* sync.next
console.log(`The number is ${n}`)
})
await Effect.runPromise(program.pipe(Effect.provide(Sync.layer))) // => undefined
// Example Output: The number is 3858843290019673
示例(定义具有生命周期控制的 Service)
import { Context, Effect, Layer, Console } from "effect"
class Scoped extends Context.Service<Scoped>()("Scoped", {
make: Effect.gen(function* () {
// Acquire the resource and ensure it is properly released
const resource = yield* Effect.acquireRelease(
Console.log("Acquiring...").pipe(Effect.as("foo")),
() => Console.log("Releasing..."),
)
// Register a finalizer to run when the effect is completed
yield* Effect.addFinalizer(() => Console.log("Shutting down"))
return { resource }
}),
}) {
static readonly layer = Layer.effect(this, this.make)
}
// ┌─── Effect<void, never, Scoped>
// ▼
const program = Effect.gen(function* () {
const resource = (yield* Scoped).resource
console.log(`The resource is ${resource}`)
})
await Effect.runPromise(
program.pipe(
Effect.provide(
// ┌─── Layer<Scoped, never, never>
// ▼
Scoped.layer,
),
),
) // => undefined
/*
Acquiring...
The resource is foo
Shutting down
Releasing...
*/
Scoped.layer Layer 不需要 Scope 作为依赖,因为 Scoped 自身管理其生命周期。