关于 effect-service-design
effect-service-design 是用于设计或审查 Effect 服务模块的技能,面向 Effect 开发者。它依据项目规范和固定的 Effect 版本,指导划分所有权边界、组合 Layer 并选择测试实现。
主要功能
- 01判断服务或值的边界
- 02规划端口与适配器归属
- 03组织 Effect 服务模块
- 04选择恰当的测试 Layer
- 05审查或设计服务
适用场景
- 01设计新的 Effect 服务
- 02检查代码库中的服务边界
- 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. 完成所选分支
- 设计分支: 记录服务或值的决策及其依据。若需要实现,则创建或重构模块、更新组合根和测试,并运行仓库要求的检查。
- 审计分支: 提供按优先级排列的发现,并附上文件/行号或符号依据、目标模块结构、组合与测试影响,以及明确的“保留”决定。
完成标准: 所设计的能力有明确的处置结论,并在需要时完成经过验证的实现;或者审计清单中的每一项都有处置结论。验证通过,或已报告所有失败。