899 words
4 minutes
[Effect Layers] 09. Effect.Service:服务和层一体化

我们可以注意到在我们定义的所有服务中有一个共同的模式:

  1. 包含服务实现的 make 函数

  2. 用于服务定义的 Context.Tag

  3. 服务类内作为 static 属性的 Live

// 1️⃣ `make` 实现
const make = /// ...

// 2️⃣ `Context.Tag` 服务
export class PokeApi extends Context.Tag("PokeApi")<
  PokeApi,
  Effect.Effect.Success<typeof make>
>() {
  // 3️⃣ `Live` 层
  static readonly Live = /// ...
}
IMPORTANT

Effect.Service 的引入

事实证明,这种模式在Effect中无处不在,不仅仅是在本课程的示例中。如此常见,以至于在v3.9中引入了一个API来简化这一切:Effect.Service

TIP

Effect.Service 的作用

Effect.Service 允许在单个类中定义默认服务实现(相当于 make)和默认层(相当于 Live 层)。

https://www.typeonce.dev/course/effect-beginners-complete-getting-started/layers/effect-service-service-and-layer-all-in-one#non-effect-services非Effect服务#

让我们从重构 PokemonCollection 服务开始。

import { Context, Layer, type Array } from "effect";

export class PokemonCollection extends Context.Tag("PokemonCollection")<
  PokemonCollection,
  Array.NonEmptyArray<string>
>() {
  static readonly Live = Layer.succeed(this, [
    "staryu",
    "perrserker",
    "flaaffy",
  ]);
}

Effect.Service 在定义上与 Context.Tag 类似:

  • 使用 class 定义

  • 需要提供服务的唯一名称(作为 string

  • 需要第一个类型参数是服务类型本身

import { Effect } from "effect";

export class PokemonCollection extends Context.Tag("PokemonCollection")< 
  PokemonCollection, 
  Array.NonEmptyArray<string> 
>() {} 

export class PokemonCollection extends Effect.Service<PokemonCollection>()(
  "PokemonCollection",
  { /* TODO */ }
) {}

我们可以简单地在第二个类型参数中提供默认实现,而不是添加 Live 并为其提供默认实现。

在这种情况下,由于层包含 Array 而不是 Effect,我们可以定义 succeed

import { Effect } from "effect";

export class PokemonCollection extends Effect.Service<PokemonCollection>()(
  "PokemonCollection",
  {
    succeed: ["staryu", "perrserker", "flaaffy"],
  }
) {}
NOTE

succeed 用于所有之前使用 Layer.succeed 定义层的服务。

这就是使用 Effect.Service 定义服务所需的全部内容。

TIP

类型推断

请注意,使用这种实现,PokemonCollection 的类型被推断为常量数组(readonly ["staryu", "perrserker", "flaaffy"])。

使用Effect定义的服务#

对于 BuildPokeApi,唯一的区别是服务被实现为 Effect

import { Context, Effect, Layer } from "effect";
import { PokeApiUrl } from "./PokeApiUrl";

export class BuildPokeApiUrl extends Context.Tag("BuildPokeApiUrl")<
  BuildPokeApiUrl,
  ({ name }: { name: string }) => string
>() {
  static readonly Live = Layer.effect(
    this,
    Effect.gen(function* () {
      const pokeApiUrl = yield* PokeApiUrl;
      return ({ name }) => `${pokeApiUrl}/${name}`;
    })
  ).pipe(Layer.provide(PokeApiUrl.Live));
}
NOTE

注意实现如何使用 Effect.gen,因此需要 Layer.effect

在这些情况下,服务在 effect 内部定义,而不是 succeed

import { Effect } from "effect";
import { PokeApiUrl } from "./PokeApiUrl";

export class BuildPokeApiUrl extends Effect.Service<BuildPokeApiUrl>()(
  "BuildPokeApiUrl",
  {
    effect: Effect.gen(function* () {
      const pokeApiUrl = yield* PokeApiUrl;
      return ({ name }: { name: string }) => `${pokeApiUrl}/${name}`;
    }),
  }
) {}

提供依赖#

我们仍然需要为服务提供所需的依赖(PokeApiUrl)。之前我们直接使用 Layer.provide

export class BuildPokeApiUrl extends Context.Tag("BuildPokeApiUrl")<
  BuildPokeApiUrl,
  ({ name }: { name: string }) => string
>() {
  static readonly Live = Layer.effect(
    this,
    Effect.gen(function* () {
      const pokeApiUrl = yield* PokeApiUrl;
      return ({ name }) => `${pokeApiUrl}/${name}`;
    })
  ).pipe(
    Layer.provide(PokeApiUrl.Live),
  );
}

Effect.Service 允许通过定义 dependencies 来做同样的事情:

export class BuildPokeApiUrl extends Effect.Service<BuildPokeApiUrl>()(
  "BuildPokeApiUrl",
  {
    effect: Effect.gen(function* () {
      const pokeApiUrl = yield* PokeApiUrl;
      return ({ name }: { name: string }) => `${pokeApiUrl}/${name}`;
    }),
    dependencies: [PokeApiUrl.Live],
  }
) {}

返回非对象值的服务#

Effect.Service 要求服务类型必须是对象。

NOTE

类型限制

具体来说,Effect.Service 接受所有可以分配给 class 声明中 implements 的类型。

//                       👇 任何在这里有效的类型
class Example implements Record<string, any> {}

在我们的示例中,PokeApiUrl 返回一个 string,这不能与 implements 一起使用:

export class PokeApiUrl extends Context.Tag("PokeApiUrl")<
  PokeApiUrl,
  string
>() {
  static readonly Live = Layer.effect(
    this,
    Effect.gen(function* () {
      const baseUrl = yield* Config.string("BASE_URL");
      return `${baseUrl}/api/v2/pokemon`;
    })
  );
}
A class cannot implement a primitive type like 'string'.
It can only implement other named object types.ts(2864)

在这些情况下,无法使用 Effect.Service,我们仍然需要回退到 Context.Tag

IMPORTANT

设计限制

请注意,在实践中,拥有返回非对象值的服务并不常见或现实。Effect.Service 专门为服务类型设计,而不是为原始值设计。

[Effect Layers] 09. Effect.Service:服务和层一体化
https://0bipinnata0.my/posts/course/effect-beginners-complete-getting-started/layers/09-effect-service-service-and-layer-all-in-one/
Author
0bipinnata0
Published at
2025-08-30 18:20:16