管理服务
了解如何在 Effect 中管理可复用的服务、高效地处理依赖,并在应用中保持整洁、解耦的架构。
在编程的语境中,服务(service)指的是可复用的组件或功能,应用的各个不同部分都可以使用它。 服务被设计用来提供特定的能力,并且可以跨多个模块或组件共享。
服务通常封装了应用不同部分都需要的常见任务或操作。 它们可以处理复杂的操作、与外部系统或 API 交互、管理数据,或执行其他专门的任务。
服务通常被设计为模块化,并且与应用的其他部分解耦。 这使得它们易于维护、测试和替换,而不会影响应用的整体功能。
在深入探讨服务以及它们在应用开发中的集成时,从函数管理和依赖处理的基本原则出发、而不依赖高级构造,会很有帮助。想象一下,你不得不手动把一个服务传递到每一个需要它的函数中:
const processData = (data: Data, databaseService: DatabaseService) => {
// Operations using the database service
}
随着应用的增长,服务需要在多层函数之间传递,这种方式会变得笨重且难以管理。
为了简化这一点,你可能会考虑使用一个环境(environment)对象来打包各种服务:
type Context = {
databaseService: DatabaseService
loggingService: LoggingService
}
const processData = (data: Data, context: Context) => {
// Using multiple services from the context
}
然而,这又引入了一个新的复杂性问题:你必须确保在使用环境之前,已经用所有必要的服务正确地配置好它,这会导致代码紧密耦合,并使函数式组合和测试变得更加困难。
使用 Effect 管理服务
Effect 库通过利用类型系统简化了这些依赖的管理。
你无需手动传递服务或环境对象,Effect 允许你使用 Effect 类型中的 Requirements 参数,直接在函数的类型签名中声明服务依赖:
┌─── Represents required dependencies
▼
Effect<Success, Error, Requirements>
在实际使用 Effect 时,它的工作方式如下:
依赖声明:你直接在类型中指明某个函数需要哪些服务,从而把依赖管理的复杂性推给类型系统。
服务提供:使用 Effect.provideService 让需要服务的函数获得某个服务的实现。在一开始就提供服务,可以确保应用的所有部分都能一致地访问所需的服务,从而保持整洁、解耦的架构。
这种方式把手工处理服务的过程抽象掉了,让开发者可以专注于业务逻辑,而由编译器来确保所有依赖都被正确处理。它也让代码更易于维护和扩展。
让我们一步一步地了解如何在 Effect 中管理服务:
- 创建服务:定义具有独特功能和接口的服务。
- 使用服务:在应用的函数中访问并使用该服务。
- 提供服务实现:提供服务的实际实现,以满足所声明的需求。
工作原理
到目前为止,我们使用 Effect 框架的示例处理的都是独立于外部服务运行的 effect。
这意味着我们 Effect 类型签名中的 Requirements 参数一直被设为 never,表示没有依赖。
然而,真实世界中的应用常常需要依赖特定服务才能正常运行的 effect。这些服务通过一个名为 Context 的构造来管理和访问。
Context 充当 effect 可能需要的所有服务的仓库或容器。
它就像一个保存这些服务的存储,让应用的各个部分都能按需访问和使用它们。
存储在 Context 中的服务会直接反映到 Effect 类型的 Requirements 参数中。
Context 中的每个服务都由一个唯一的 “tag”(标签)标识,tag 本质上就是该服务的唯一标识符。
当某个 effect 需要使用特定服务时,该服务的 tag 会被包含在 Requirements 类型参数中。
创建服务
要创建一个新服务,你需要两样东西:
- 一个唯一的标识符。
- 一个描述该服务可能操作的类型。
示例(定义一个随机数生成服务)
让我们创建一个用于生成随机数的服务。
- 标识符。我们将使用字符串
"MyRandomService"作为唯一标识符。 - 类型。该服务类型将包含一个名为
next的单一操作,它返回一个随机数。
import { Effect, Context } from "effect"
// Declaring a tag for a service that generates random numbers
class Random extends Context.Tag("MyRandomService")<
Random,
{ readonly next: Effect.Effect<number> }
>() {}
导出的 Random 值在 Effect 中被称为 tag。它代表该服务,让 Effect 能够在运行时定位并使用这个服务。
该服务将被存储在一个名为 Context 的集合中,你可以把它看作一个 Map,其中键是 tag,值是服务:
type Context = Map<Tag, Service>
你需要指定一个标识符,才能让该 tag 成为全局的。这确保了两个具有相同标识符的 tag 指向同一个实例。
在可能发生热重载的场景中,使用唯一标识符尤其有用,因为它有助于在重载之间保留实例。它还能确保实例不会被重复创建(虽然这不应该发生,但某些打包工具和框架的行为可能不可预测)。
让我们总结一下目前涉及的概念:
| 概念 | 说明 |
|---|---|
| 服务 | 提供特定功能、在应用的不同部分之间复用的可复用组件。 |
| tag | 代表某个服务的唯一标识符,让 Effect 能够定位并使用它。 |
| context | 存储服务的集合,其作用类似于一个以 tag 为键、以服务为值的 map。 |
使用服务
现在我们已经定义好了服务的 tag,接下来通过构建一个简单的程序来看看如何使用它。
示例(使用 Random 服务)
import { Effect, Context } from "effect"
// Declaring a tag for a service that generates random numbers
class Random extends Context.Tag("MyRandomService")<
Random,
{ readonly next: Effect.Effect<number> }
>() {}
// Using the service
//
// ┌─── Effect<void, never, Random>
// ▼
const program = Effect.gen(function* () {
const random = yield* Random
const randomNumber = yield* random.next
console.log(`random number: ${randomNumber}`)
})在上面的代码中,我们可以看到,我们能够像 yield 一个 effect 一样 yield Random tag。
这让我们能够访问该服务的 next 操作。
import { Effect, Context, Console } from "effect"
// Declaring a tag for a service that generates random numbers
class Random extends Context.Tag("MyRandomService")<
Random,
{ readonly next: Effect.Effect<number> }
>() {}
// Using the service
//
// ┌─── Effect<void, never, Random>
// ▼
const program = Random.pipe(
Effect.andThen((random) => random.next),
Effect.andThen((randomNumber) =>
Console.log(`random number: ${randomNumber}`),
),
)在上面的代码中,我们可以看到,我们能够像对 effect 本身一样对 Random tag 进行 flat-map。
这让我们能够在 Effect.andThen 回调中访问该服务的 next 操作。
值得注意的是,program 变量的类型在 Requirements 类型参数中包含了 Random:
const program: Effect<void, never, Random>
这表明我们的程序需要提供 Random 服务才能成功执行。
如果我们尝试在没有提供必要服务的情况下执行该 effect,就会遇到类型检查错误:
示例(未提供服务时的类型错误)
import { Effect, Context } from "effect"
// Declaring a tag for a service that generates random numbers
class Random extends Context.Tag("MyRandomService")<
Random,
{ readonly next: Effect.Effect<number> }
>() {}
// Using the service
const program = Effect.gen(function* () {
const random = yield* Random
const randomNumber = yield* random.next
console.log(`random number: ${randomNumber}`)
})
// @errors: 2379
Effect.runSync(program)
要解决这个错误并成功执行程序,我们需要为 Random 服务提供一个实际的实现。
在下一节中,我们将探讨如何实现 Random 服务并将其提供给我们的程序,从而让它成功运行。
提供服务实现
为了给 Random 服务提供一个实际的实现,我们可以使用 Effect.provideService 函数。
示例(提供一个随机数实现)
import { Effect, Context } from "effect"
// Declaring a tag for a service that generates random numbers
class Random extends Context.Tag("MyRandomService")<
Random,
{ readonly next: Effect.Effect<number> }
>() {}
// Using the service
const program = Effect.gen(function* () {
const random = yield* Random
const randomNumber = yield* random.next
console.log(`random number: ${randomNumber}`)
})
// Providing the implementation
//
// ┌─── Effect<void, never, never>
// ▼
const runnable = Effect.provideService(program, Random, {
next: Effect.sync(() => Math.random()),
})
// Run successfully
Effect.runPromise(runnable)
/*
Example Output:
random number: 0.8241872233134417
*/
在上面的代码中,我们为之前定义的 program 提供了 Random 服务的一个实现。
我们使用 Effect.provideService 函数把 Random tag 与它的实现关联起来,该实现是一个带有 next 操作、用于生成随机数的对象。
注意,现在 runnable effect 的 Requirements 类型参数是 never。这表明该 effect 不再需要提供任何服务。
有了 Random 服务的实现,我们就能够在没有任何额外需求的情况下运行这个程序了。
提取服务类型
要从 tag 中取出服务类型,请使用 Context.Tag.Service 工具类型。
示例(提取服务类型)
import { Effect, Context } from "effect"
// Declaring a tag
class Random extends Context.Tag("MyRandomService")<
Random,
{ readonly next: Effect.Effect<number> }
>() {}
// Extracting the type
type RandomShape = Context.Tag.Service<Random>
/*
This is equivalent to:
type RandomShape = {
readonly next: Effect.Effect<number>;
}
*/
使用多个服务
当我们需要使用多个服务时,流程与我们在定义服务时学到的基本相同,只需对每个所需的服务重复一遍即可。
示例(同时使用 Random 和 Logger 服务)
让我们看一个需要两个服务的例子,即 Random 和 Logger:
import { Effect, Context } from "effect"
// Declaring a tag for a service that generates random numbers
class Random extends Context.Tag("MyRandomService")<
Random,
{
readonly next: Effect.Effect<number>
}
>() {}
// Declaring a tag for the logging service
class Logger extends Context.Tag("MyLoggerService")<
Logger,
{
readonly log: (message: string) => Effect.Effect<void>
}
>() {}
const program = Effect.gen(function* () {
// Acquire instances of the 'Random' and 'Logger' services
const random = yield* Random
const logger = yield* Logger
const randomNumber = yield* random.next
yield* logger.log(String(randomNumber))
})
现在 program effect 的 Requirements 类型参数是 Random | Logger:
const program: Effect<void, never, Random | Logger>
这表明它需要同时提供 Random 和 Logger 这两个服务。
要执行 program,我们需要为这两个服务都提供实现:
示例(提供多个服务)
import { Effect, Context } from "effect"
// Declaring a tag for a service that generates random numbers
class Random extends Context.Tag("MyRandomService")<
Random,
{
readonly next: Effect.Effect<number>
}
>() {}
// Declaring a tag for the logging service
class Logger extends Context.Tag("MyLoggerService")<
Logger,
{
readonly log: (message: string) => Effect.Effect<void>
}
>() {}
const program = Effect.gen(function* () {
const random = yield* Random
const logger = yield* Logger
const randomNumber = yield* random.next
return yield* logger.log(String(randomNumber))
})
// Provide service implementations for 'Random' and 'Logger'
const runnable = program.pipe(
Effect.provideService(Random, {
next: Effect.sync(() => Math.random()),
}),
Effect.provideService(Logger, {
log: (message) => Effect.sync(() => console.log(message)),
}),
)
或者,我们不必多次调用 provideService,而是可以把这些服务实现合并到一个单独的 Context 中,然后使用 Effect.provide 函数提供整个 context:
示例(合并服务实现)
import { Effect, Context } from "effect"
// Declaring a tag for a service that generates random numbers
class Random extends Context.Tag("MyRandomService")<
Random,
{
readonly next: Effect.Effect<number>
}
>() {}
// Declaring a tag for the logging service
class Logger extends Context.Tag("MyLoggerService")<
Logger,
{
readonly log: (message: string) => Effect.Effect<void>
}
>() {}
const program = Effect.gen(function* () {
const random = yield* Random
const logger = yield* Logger
const randomNumber = yield* random.next
return yield* logger.log(String(randomNumber))
})
// Combine service implementations into a single 'Context'
const context = Context.empty().pipe(
Context.add(Random, { next: Effect.sync(() => Math.random()) }),
Context.add(Logger, {
log: (message) => Effect.sync(() => console.log(message)),
}),
)
// Provide the entire context
const runnable = Effect.provide(program, context)
可选服务
有些情况下,我们可能只想在服务实现可用时才访问它。
在这种情况下,我们可以使用 Effect.serviceOption 函数来处理这种场景。
Effect.serviceOption 函数返回一个实现,只有当它确实在执行该 effect 之前被提供时,它才是可用的。
为了表示这种可选性,它返回该实现的一个 Option。
示例(处理可选服务)
为了决定采取什么操作,我们可以使用 Option 模块提供的 Option.isNone 函数。当服务不可用时,该函数会返回 true,借此我们可以检查服务是否可用。
import { Effect, Context, Option } from "effect"
// Declaring a tag for a service that generates random numbers
class Random extends Context.Tag("MyRandomService")<
Random,
{ readonly next: Effect.Effect<number> }
>() {}
const program = Effect.gen(function* () {
const maybeRandom = yield* Effect.serviceOption(Random)
const randomNumber = Option.isNone(maybeRandom)
? // the service is not available, return a default value
-1
: // the service is available
yield* maybeRandom.value.next
console.log(randomNumber)
})
在上面的代码中,我们可以看到,即使我们正在使用一个服务,program effect 的 Requirements 类型参数仍然是 never。这让我们只有在服务确实于执行该 effect 之前被提供时,才能从 context 中访问到东西。
当我们在不提供 Random 服务的情况下运行 program effect 时:
Effect.runPromise(program).then(console.log)
// Output: -1
我们看到日志消息中包含 -1,这正是我们在服务不可用时提供的默认值。
然而,如果我们提供 Random 服务的实现:
Effect.runPromise(
Effect.provideService(program, Random, {
next: Effect.sync(() => Math.random()),
}),
).then(console.log)
// Example Output: 0.9957979486841035
我们可以看到,日志消息现在包含一个由 Random 服务的 next 操作生成的随机数。
处理带依赖的服务
有时应用中的某个服务可能依赖其他服务。为了保持整洁的架构,重要的是在不把这些依赖暴露到服务接口中的情况下管理它们。相反,你可以在服务构建阶段使用 Layer 来处理这些依赖。
示例(定义一个带配置依赖的 Logger 服务)
考虑一个多个服务相互依赖的场景。在这个例子中,Logger 服务需要访问一个配置服务(Config)。
import { Effect, Context } from "effect"
// Declaring a tag for the Config service
class Config extends Context.Tag("Config")<Config, {}>() {}
// Declaring a tag for the logging service
class Logger extends Context.Tag("MyLoggerService")<
Logger,
{
// ❌ Avoid exposing Config as a requirement
readonly log: (message: string) => Effect.Effect<void, never, Config>
}
>() {}
为了以结构化的方式处理这些依赖,并防止它们泄漏到服务接口中,你可以使用 Layer 抽象。关于使用 Layer 管理依赖的更多细节,请参阅 管理 Layer 页面。
当一个服务有自己的 Requirements 时,最好把实现细节分离到 Layer 中。Layer 充当创建该服务的构造器,让我们能在构造层面而非服务层面处理依赖。