Specification pattern 的例子常常从可复用 predicate object 开始。Appaloft 大多数时候用得更轻:用命名的 spec 表达 selection、readiness、compatibility 或 policy decision,但不执行副作用。
最后这点最重要。Specification 可以说要选什么、要求是否满足、缺什么 evidence。它不应该悄悄 mutate deployment、创建 provider resource,或者修复 drift。
Selection spec 代替模糊查询参数
Installed application repository 使用命名 selection spec。Single selection 可以按 application id,也可以按 idempotency key;many selection 可以按 marketplace slug,也可以是 all。
这不复杂,但很有用。它避免了那种有十个 optional field、优先级全靠约定的 TypeScript query object。
当 repository 收到 { kind: "idempotency-key", idempotencyKey },意图是清楚的。当它收到 { kind: "marketplace-slug", marketplaceSlug },调用方不是在依赖 nullable applicationId 加另一个 marketplaceSlug 的隐式约定。
小 spec 会让测试更好读,也更不容易误扩大查询范围。
Repository 收到的不是一袋 loose optional filters,而是一个被命名的 selection:
export type CloudInstalledApplicationSingleSelectionSpec =
| { readonly kind: "application-id"; readonly applicationId: string }
| { readonly kind: "idempotency-key"; readonly idempotencyKey: string };
export type CloudInstalledApplicationManySelectionSpec =
| { readonly kind: "marketplace-slug"; readonly marketplaceSlug: string }
| { readonly kind: "all" };
Readiness spec 把 gate 说出来
Dependency provisioning 也有 specification 风格的输入。Plan 可以包含 version requirement、capability、readiness requirement、provider key 和 mode。Workflow 再推导 required acknowledgement 与 readback check。
重点是:unsupported capability 或缺 provider support 不应该藏在 provider exception 里。系统可以返回 adapter_unsupported、production apply gate blocked decision,或者带明确 evidence 的 status/readback shape。
比如 production external apply gate 会返回 blocked decision,列出 owner-approved runbook、provider readback evidence、secret-ref evidence、rollback-delete evidence 等 required evidence。这个 decision 不创建资源,它只是表达 policy。
这个 policy 返回的是调用方可以渲染或审计的数据,而不是一个半创建的 provider resource:
export function evaluateCloudDependencyProductionExternalApplyGate(input: {
readonly kind: CloudDependencyProvisioningPlanInput["kind"];
readonly providerKey: string;
}): CloudDependencyProductionExternalApplyGateDecision {
return {
schemaVersion: "appaloft.cloud.dependency-provisioning.external-apply-gate/v1",
status: "blocked_on_owner_approval",
kind: normalizeKind(input.kind),
providerKey: input.providerKey,
createsExternalResources: false,
requiredEvidence: [
"owner-approved-provider-runbook",
"provider-readback-evidence",
"secret-ref-evidence",
"rollback-delete-evidence",
],
reason: "paid_or_production_provider_apply_requires_owner_approval",
};
}
Drift report 是 specification,不是 command
docs/cloud/code-as-infra.md 里有一条很容易在自动化里丢掉的线:drift detection 可以比较 observed state 和 target evidence,但 repair 必须走 explicit command。
这就是 specification 思路。Drift report 可以描述 finding、hash value、总结缺失资源、说明 observed state 和 portable spec 哪里不同。它不能变成隐藏的 command handler。
对 deployment control plane 来说这尤其重要。如果 projection 或 report 可以 mutate 基础设施,operator 就很难把 plan、acceptance、apply、verify 和 rollback 当作几个可 review 的时刻。
Specification 可以是普通数据
Appaloft 不要求每个 specification 都写成带 isSatisfiedBy 的 class。TypeScript discriminated union 和 readonly input object 往往更合适。
Spec 有价值,是因为它命名了一个意图:
- 用 idempotency key 选择这个 installed application
- 列出某个 marketplace slug 对应的 installed applications
- 要求某个 dependency capability
- 在 evidence 不足时阻止 provider apply
- 把 observed runtime state 和 portable spec 做比较
实现可以是 function、class、repository method 或 workflow step。DDD 的部分,是被命名的意图和副作用边界。
失败模式
失败模式是把所有参数对象都叫 specification,最后什么也没变好。Appaloft 会尽量把这个模式留给歧义代价高的地方:repository selection、readiness gate、provider support、drift detection 和 policy check。
这样用时,Specification 不太像抽象炫技,更像一种运维诚实。它告诉系统知道什么、不知道什么,以及在改变世界之前还缺什么。