mcpField
E
effect-service-design

effect-service-design

github.com/dmmulroy开发432200 次安装14 周前更新
代码仓库

关于 effect-service-design

effect-service-design 是用于设计或审查 Effect 服务模块的技能,面向 Effect 开发者。它依据项目规范和固定的 Effect 版本,指导划分所有权边界、组合 Layer 并选择测试实现。

主要功能

  1. 01判断服务或值的边界
  2. 02规划端口与适配器归属
  3. 03组织 Effect 服务模块
  4. 04选择恰当的测试 Layer
  5. 05审查或设计服务

适用场景

  1. 01设计新的 Effect 服务
  2. 02检查代码库中的服务边界
  3. 03选择生产与测试 Layer 的组合方式

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/dmmulroy/skills --skill effect-service-design

安装后在 Claude Code 中输入/effect-service-design确认已加载。

附带文件agents/openai.yamlreferences/AUDIT.md

详细介绍

Effect 服务设计

将服务视为一个权责边界:它是一项内聚的能力,其需求应通过 Effect 上下文逐层传递。一个 Effect 服务模块负责服务契约、构造方式、生产环境 Layer,以及属于同一能力的任何可靠且可复用的测试实现。

1. 确立本地规则

阅读最近的 AGENTS.md、架构文档、编码规范、项目的 Effect 指引、固定的 Effect 版本/源码,以及相关的 vendored 示例。优先采用与当前项目兼容的模式,而不是凭记忆使用 API。

**完成条件:**已阅读并记录适用的文件和固定源码示例,供后续决策参考。

2. 选择分支

  • **设计分支:**适用于新服务或聚焦式重设计。界定能力范围,追踪一个调用方可见的操作及其涉及的每项 Effect,然后继续执行以下步骤。
  • **审计分支:**适用于代码库、包、功能切片或 diff。阅读并遵循 references/AUDIT.md,将以下规则应用于其中发现的每个候选项。

**完成条件:**已选定一个分支、界定其范围,并列出范围内调用方可见的操作。

3. 应用服务判定标准

真正的服务至少负责一项有意义的能力:

  • 对持久化、凭据、外部 I/O、运行时资源、配置、时间、随机性或生命周期拥有控制权;
  • 在多个入口点复用内聚的 Effect 执行顺序或策略;
  • 具有生产环境与测试/运行时环境之间确实存在差异的状态或行为;
  • 实现复杂度足够高,删除该模块会将复杂性转移到调用方。

定义应用服务前,优先使用现有的 Effect 服务,例如 Clock、Crypto、Random、Config、HttpClient、FileSystem 或 Path。

以下内容应保留为值或纯模块:

  • 已解析的领域输入和单次调用的请求数据;
  • 确定性计算、投影、解析器和构造器;
  • 为单次调用选择策略的选项;
  • 局限于其适配器内部的框架值;
  • 仅重命名或转发其他服务的包装器。

仅仅因为测试希望注入某个值,并不足以定义服务。这个边界必须体现生产环境中的真实职责或可变性。

**完成条件:**删除测试和现有服务/适配器审查都支持“服务”或“值”这一结论,并明确说明被否决的选项。

4. 设置权责边界

  • 领域模块保持纯粹。
  • 应用服务负责操作策略和应用侧端口。
  • 端口的 Tag 和接口与需要它的应用操作放在一起。
  • 具体适配器负责其技术栈特有的 make 和 layer;它不必与应用侧端口共用一个文件。
  • 组合根选择并提供具体的 Layer,不承担可复用的策略模块职责。
  • 在组合根或所属适配器中生成运行时绑定,然后通过应用/领域类型将其隐藏。

构建 Layer 时生成稳定的依赖项,并在服务方法中闭包捕获它们。在使用请求级、fiber 级或操作级上下文的方法内部生成这些上下文。让需求逐层传递,直到真正负责选择实现的模块提供它们。

只要所属适配器已生成相关运行时能力,将外部库的构造选项传入该库仍然是正确的。React props、请求值、领域输入和框架构造器都不属于 Effect 依赖注入。

**完成条件:**依赖指向内层,原始技术类型止于适配器,并且没有内层调用方选择其无权负责的具体实现。

5. 塑造 Effect 服务模块

遵循项目既定的等效结构:

export interface Interface {
  readonly operation: (input: Input) => Effect.Effect<Output, OperationError>
}

export class Service extends Context.Service<Service, Interface>()(
  "@app/Capability",
) {}

export const make: Effect.Effect<
  Service["Service"],
  never,
  Dependency.Service
> = Effect.gen(function* () {
  const dependency = yield* Dependency.Service

  const operation = Effect.fn("Capability.operation")(function* (input: Input) {
    return yield* dependency.operation(input)
  })

  return Service.of({ operation })
})

export const layerWithoutDependencies = Layer.effect(Service, make)

export const layer = layerWithoutDependencies.pipe(
  Layer.provide([Dependency.layer]),
)

export const layerTest = Layer.succeed(
  Service,
  Service.of({
    operation: (_input) => Effect.succeed(testOutput),
  }),
)

layerWithoutDependencies 保留服务用于组合的依赖项。layer 是可直接用于生产环境的组装,并提供具体的依赖 Layer。layerTest 展示完整的静态替代实现;仅当该行为可复用且确实符合实际情况时才导出。当内存实现能够忠实保留可观察到的契约时,应改用 layerMemory。

对已构建的值使用 Layer.succeed,对惰性同步构造使用 Layer.sync,对有副作用的获取使用 Layer.effect。如果改名不会带来更清晰的表达,保留已确立且兼容的命名约定。

保持接口精简且贴合领域。使用命名的 Effect.fn 方法、带类型的预期错误,以及 yield 注入的依赖。只有在选项、方法、服务和组合器各自足以隐藏复杂性、值得引入时,才添加它们。

完成标准: 标签、接口、make、保留依赖项的 Layer、生产环境 layer、错误、方法和测试策略都有明确的归属方,并且每个导出的符号都为调用方所需。

6. 诚实地选择测试 Layer

  • 使用 Layer.succeed 实现完整的静态实现。
  • 当可复用状态、故障注入或观测能力构成真实的扩展点时,添加 layerTest 和测试控制服务。
  • 只有当某个 Layer 能在内存中忠实实现服务的可观察契约时,才将其命名为 layerMemory。
  • 当持久化、事务、序列化或协议行为很重要时,优先选择真实的本地替代方案。
  • 如果将一次性小型 fake 提升为独立实现会仅为该测试增加生产代码接口,就将其留在对应的测试中。

测试应与生产环境调用方使用相同的服务接口。当存在可复用的控制服务时,生产标签和测试控制标签应由同一个对象支持。包含会因“未使用”方法而失败的不完整对象,应作为专用测试夹具,而非可复用的内存适配器。

完成标准: 每个测试实现都完整符合其声明的名称;测试通过公共接口观测结果;没有仅为支持模拟而新增的扩展点。

7. 完成所选分支

  • 设计分支: 记录服务或值的决策及其依据。若需要实现,则创建或重构模块、更新组合根和测试,并运行仓库要求的检查。
  • 审计分支: 提供按优先级排列的发现,并附上文件/行号或符号依据、目标模块结构、组合与测试影响,以及明确的“保留”决定。

完成标准: 所设计的能力有明确的处置结论,并在需要时完成经过验证的实现;或者审计清单中的每一项都有处置结论。验证通过,或已报告所有失败。

评论

支持 Markdown