specification 接口应按领域实体定义具体类型(如 orderspec),避免 interface{};组合用闭包实现短路逻辑;与 validator 职责分离,前者管业务语义,后者管数据格式;测试需覆盖业务假设而非仅数值边界。

Specification 接口怎么定义才不和业务耦合
Go 里没有泛型约束前(1.18 之前),直接写 type Specification interface { IsSatisfiedBy(v interface{}) bool } 看似通用,实则埋雷:调用方必须做类型断言,一不小心就 panic;而且 IDE 完全无法推导参数类型,写起来像盲打。
更稳妥的做法是按领域实体定义具体接口,比如订单场景下用 type OrderSpec interface { IsSatisfiedBy(o *Order) bool }。这样既保留编译期类型检查,又避免泛型泛滥带来的理解成本。
- 不要用
interface{}做参数,哪怕只是临时省事——它会让校验逻辑悄悄脱离类型系统 - 如果多个实体共用相似规则(如“金额大于 X”),抽成函数工厂,而不是强行统一接口:
func AmountGreaterThan(threshold float64) func(*Order) bool - 别在
IsSatisfiedBy里做副作用(如打日志、发消息),它只该回答“是/否”,否则组合时行为不可预测
多个 Specification 怎么安全组合(And/Or/Not)
手写 And 组合器时最容易犯的错,是忽略短路逻辑或 panic 传播。比如两个 OrderSpec 组合后,第一个返回 false 就该直接结束,而不是继续执行第二个——后者可能依赖前一个的前置状态(如非空字段)。
推荐用闭包封装组合逻辑,而非结构体 + 方法。简洁、无状态、易测试:
立即学习“go语言免费学习笔记(深入)”;
func And(a, b OrderSpec) OrderSpec {
return func(o *Order) bool {
return a(o) && b(o) // 天然短路
}
}
func Or(a, b OrderSpec) OrderSpec {
return func(o *Order) bool {
return a(o) || b(o)
}
}
- 不用结构体实现组合器(如
type AndSpec struct{ a, b OrderSpec }),它徒增内存分配且掩盖组合意图 -
Not要小心 nil 检查:如果传入的 spec 是 nil,Not(nil)应该 panic 还是返回恒真?建议 panic,因为 nil spec 本身已是编程错误 - 组合深度超过 3 层时,考虑提前命名中间结果(如
validPaymentOrder := And(hasPaid, notRefunded)),别堆一行嵌套
Specification 和 validator 库(如 go-playground/validator)冲突吗
不冲突,但职责必须划清:go-playground/validator 解决的是“数据格式是否合法”(字段非空、长度、正则),而 Specification 解决的是“这个对象是否符合当前业务语义”(如“用户可下单”=余额充足+未被冻结+地址已认证)。
混用时典型错误是把字段级校验塞进 Specification,导致规则分散、复用困难。比如 EmailFormatSpec 就不该存在——交给 validator 的 email tag 即可。
- Specification 只接收完整结构体指针(如
*Order),绝不接收单个字段值 - 如果某条规则需访问外部服务(如查风控状态),Specification 内部应接受依赖(如
func NewRiskApprovedSpec(client RiskClient) OrderSpec),而不是硬编码 HTTP 调用 - validator 的 struct tag 校验失败会返回
error,Specification 必须返回bool;两者不能互相替代,但可以串联:先ValidateStruct,再spec.IsSatisfiedBy(order)
为什么单元测试 Specification 很容易漏掉边界情况
因为人天然倾向测“正常路径”。比如写 OrderAmountOver1000Spec,多数人只测 1001 和 999,却忘了 0、负数、NaN(如果金额是 float64)、甚至 nil 订单指针。
真正有效的测试不是覆盖所有输入,而是覆盖规则背后的业务假设。例如“金额大于 1000 才触发风控”这条规则,关键不是数字比较,而是:谁设的阈值?会不会动态变化?货币单位是否统一?这些都要在测试用例名里暴露出来。
- 每个 Specification 测试文件以
xxx_spec_test.go命名,和主逻辑放一起,避免“规则写了,测试找不到” - 用表驱动测试,但 case 名必须含业务含义,比如
{"order with USD currency and amount 1001 should trigger"},而不是{"input: 1001, want: true"} - 不要 mock 外部依赖来“让测试快”——如果一条规则依赖库存服务,就起个本地 fake server,否则你永远不知道网络超时或空响应时规则是否还成立
Specification 模式真正的难点不在写接口,而在于每次新增规则时,要同步更新它的组合方式、测试边界、以及文档注释里那句“此规则生效的前提是……”。少写一行前提说明,半年后你就得重读三天代码才能改对。










