用途
Typed Service Contracts(Spec 与 Handler 模式)是一套架构标准,用于构建健壮、类型安全的 TypeScript 服务。它定义了一种由契约式设计(Design by Contract,DbC)原则支撑的垂直切片架构,把应用逻辑视为严格定义的工作单元:输入被解析(parse)而非仅仅校验(validate),错误被当作值(Result 模式)处理,而不是抛出异常。
适用场景
- 构建 CLI 或库:需要在用户输入与系统逻辑之间建立严格边界时。
- 复杂校验:当输入在被使用前需要转换(解析)时,例如确保某个字符串是有效的文件路径。
- 高可靠性要求:当无法承受未处理的运行时异常、需要穷尽的错误处理时。
- 测试聚焦:当希望将数据校验测试与业务逻辑测试分离时。
架构组件
1. Spec(spec.ts)
即“契约”或“端口”,定义“做什么”。它必须包含:
- 输入 Schema:一个 Zod schema,将原始输入解析为有效的 DTO。
- 输出 Schema:一个 Zod schema,定义成功时的数据结构。
- 错误 Schema:具体失败模式的可辨识联合(discriminated union),而非泛化错误。
- Result 类型:Success | Failure 的可辨识联合。
- 接口:能力定义,例如 interface ConfigureSpec。
2. Handler(handler.ts)
即“实现”或“适配器”,定义“怎么做”。它必须:
- 实现 Spec 中定义的接口。
- 是一个处理副作用(文件系统、API 调用)的“非纯”类。
- 绝不抛出异常。它必须捕获内部错误并将其映射为 Result 类型。
使用方式
第 1 步:定义契约(spec.ts)
按照以下模板定义边界。
import { z } from 'zod';
// 1. VALIDATION HELPERS (Reusable Refinements)
export const SafePathSchema = z.string()
.min(1)
.refine(p => !p.includes('..'), "No traversal allowed");
// 2. INPUT (The Command) - "Parse, don't validate"
export const MyTaskInputSchema = z.object({
path: SafePathSchema,
force: z.boolean().default(false),
});
export type MyTaskInput = z.infer<typeof MyTaskInputSchema>;
// 3. ERROR CODES (Exhaustive)
export const MyTaskErrorCode = z.enum([
'FILE_NOT_FOUND',
'PERMISSION_DENIED',
'UNKNOWN_ERROR'
]);
// 4. RESULT (The Monad)
export const MyTaskSuccess = z.object({
success: z.literal(true),
data: z.string(), // The output payload
});
export const MyTaskFailure = z.object({
success: z.literal(false),
error: z.object({
code: MyTaskErrorCode,
message: z.string(),
suggestion: z.string().optional(),
recoverable: z.boolean(),
})
});
export type MyTaskResult =
| z.infer<typeof MyTaskSuccess>
| z.infer<typeof MyTaskFailure>;
// 5. INTERFACE (The Capability)
export interface MyTaskSpec {
execute(input: MyTaskInput): Promise<MyTaskResult>;
}
第 2 步:实现 Handler(handler.ts)
按照以下模板实现逻辑。
import { MyTaskSpec, MyTaskInput, MyTaskResult } from './spec.js';
import * as fs from 'fs';
export class MyTaskHandler implements MyTaskSpec {
async execute(input: MyTaskInput): Promise<MyTaskResult> {
try {
// 1. Business Logic
if (!fs.existsSync(input.path)) {
// 2. Explicit Error Return (No Throwing)
return {
success: false,
error: {
code: 'FILE_NOT_FOUND',
message: `Path does not exist: ${input.path}`,
recoverable: true
}
};
}
// 3. Success Return
return {
success: true,
data: 'Operation complete'
};
} catch (error) {
// 4. Safety Net: Catch unknown runtime errors
return {
success: false,
error: {
code: 'UNKNOWN_ERROR',
message: error instanceof Error ? error.message : String(error),
recoverable: false
}
};
}
}
}
第 3 步:测试策略
不要编写单体测试,而应将其拆分为契约测试与逻辑测试。
A. 契约测试(Schema)
测试“门卫”,确保无效数据在到达 handler 之前被拒绝。
- 聚焦:边界情况、校验规则、Zod refinements。
- 风格:数据驱动(表驱动测试)。
// spec.test.ts
import { MyTaskInputSchema } from './spec';
const invalidCases = [
{ val: '../etc/passwd', err: 'No traversal allowed' },
{ val: '', err: 'min(1)' },
];
test.each(invalidCases)('validates paths', ({ val, err }) => {
const result = MyTaskInputSchema.safeParse({ path: val });
expect(result.success).toBe(false);
});
B. 逻辑测试(Handler)
测试“厨师”,模拟外部依赖(fs、网络)并断言 Result 对象。
- 聚焦:业务逻辑流程、错误映射、成功状态。
- 风格:模拟单元测试或场景运行器。
// handler.test.ts
import { MyTaskHandler } from './handler';
import { vi } from 'vitest'; // or jest
test('returns FILE_NOT_FOUND if path missing', async () => {
// MOCK
vi.mocked(fs.existsSync).mockReturnValue(false);
// EXECUTE
const handler = new MyTaskHandler();
const result = await handler.execute({ path: '/fake' });
// ASSERT (Check the Result Object)
expect(result.success).toBe(false);
if (!result.success) {
expect(result.error.code).toBe('FILE_NOT_FOUND');
}
});
限制
资料未提供关于该技能的具体安装命令、依赖版本要求、平台限制或收费信息,暂未提供。