Effect 深度课effect@4.1.4 · LTS
依赖与资源

L13 Context.Service:类型化的服务键

⏱ 25 min难度 2/5前置:L07
本课目标
  • 会定义并消费一个服务
  • 理解 R 通道与 Context 的关系
前置课时尚未完成:L07

可以自行跳转学习,但按顺序推进会更省力;学情分析会把这类缺口归入「前置缺口」。

服务 = 键 + 形状

Context.Service 把服务标识和实现形状定义成一个类。Context 则是以这些键为索引的不可变服务集合。程序通过 yield* SomeService 或 SomeService.use(...) 声明「我需要它」,编译器把该服务记进 R。

定义、使用、提供
← 代码可左右滑动 →
import { Context, Effect, Layer } from "effect"

class Notifications extends Context.Service<
  Notifications,
  { readonly notify: (msg: string) => Effect.Effect<void> }
>()("Notifications") {}

Notifications.key // "Notifications"

const action = Notifications.use((n) => n.notify("hello"))
// Effect<void, never, Notifications>

const runnable = action.pipe(
  Effect.provide(Layer.succeed(Notifications, {
    notify: (msg) => Effect.log(msg)
  }))
)
// Effect<void, never, never>
💡 服务形状里的方法返回的仍是 Effect,于是错误与依赖可以继续向外累积。
💡 端口与适配器

把 Context.Service 当作端口(接口),把 Layer 当作适配器(实现)。用例代码只依赖端口,测试里换一个 Layer.succeed 就能替换整个外部系统,无需 mock 框架。

交互演练
关键术语
Context.Service
v4 中定义服务键与形状的类
Context.make
手动构造一个 Context<R>
常见误区
× 把服务实例塞进模块级单例,绕开 R 通道,测试时无法替换。
随堂测验(即时反馈,答题计入掌握度)
Q1yield* SomeService 之后,程序类型发生什么变化?
Q2替换服务的实现应该改哪里?