HelloWorld 规格模式教程
规格模式把业务规则封装成小而明确的“判断单元”,通过接口和组合逻辑把复杂条件拼起来,既能复用又便于测试。本文用一个HelloWorld级别的例子,从概念、接口设计、组合器、三种语言实现(Java、C#、JavaScript)、单元测试到工程注意点,逐步演示如何把抽象规则变成可维护、可组合的代码,顺便讨论性能与常见陷阱,帮助你在真实项目中落地。

先把概念说清楚:规格模式到底是什么?
简单来说,规格模式(Specification Pattern)把“是否满足某个业务条件”抽象为一个对象,它通常提供一个方法(比如 isSatisfiedBy)来判断某个候选对象是否满足该条件。把规则封成对象后,你可以:
- 把规则像搭积木一样组合(并且、或者、非);
- 把规则独立测试;
- 在需要时把规则转换为查询表达式(比如数据库查询或流过滤器)。
用费曼法再解释一遍:想象你在家里检查“物品是否可以带上飞机”的规则。每条规则是一个检查项,比如“液体小于100ml”“电池装好”“没有危险品”。把每条检查项做成一张小卡片(规格),你可以单独验证某张卡片,也可以把几张卡片合成一套检查流程。规格模式就是把编程里的“检查项”做成卡片。
规格的核心要素
接口与实现
核心通常只有一个接口/抽象类,约定一个判断方法,例如:
- isSatisfiedBy(candidate):直接对对象做布尔判断。
- 有时会提供 toPredicate 或 toExpression,用于把规格转成可组合的查询表达式(便于数据库或框架优化)。
组合算子
常见的组合算子是 AND、OR、NOT。实现方式通常是把两个或多个规格包装成组合规格,组合规格内部会调用子规格的 isSatisfiedBy 并返回组合结果。
HelloWorld 实战:问题设定
示例目标很简单:假设我们有一组字符串消息,想筛选出“合格的 HelloWorld 消息”。合格条件举例:
- 非空且不全是空白;
- 包含单词 “Hello” 或者 “Hi”;
- 长度不超过 100 字符;
- 不包含敏感词(模拟规则)。
把每条规则做成独立规格,再用组合规格把它们拼起来,就是这个练习的全部要点。下面我会分别展示 Java、C# 和 JavaScript 的实现思路,保留适当简化以便专注设计。
Java 实现(示例)
Java 常见的做法是定义一个泛型接口:
public interface Specification {
boolean isSatisfiedBy(T candidate);
default Specification and(Specification other) { ... }
default Specification or(Specification other) { ... }
default Specification not() { ... }
}
示例实现(部分):
public class NotEmptySpecification implements Specification {
public boolean isSatisfiedBy(String s) {
return s != null && !s.trim().isEmpty();
}
}
public class ContainsWordSpecification implements Specification {
private final String word;
public ContainsWordSpecification(String word) { this.word = word; }
public boolean isSatisfiedBy(String s) {
return s != null && s.contains(word);
}
}
public class LengthLessThanSpecification implements Specification {
private final int max;
public LengthLessThanSpecification(int max) { this.max = max; }
public boolean isSatisfiedBy(String s) {
return s != null && s.length() <= max;
}
}
组合实现通常直接在接口里用 lambda 或匿名类返回新的规格,或用具体的组合类:
public class AndSpecification implements Specification {
private final Specification a, b;
public AndSpecification(Specification a, Specification b) { this.a = a; this.b = b; }
public boolean isSatisfiedBy(T candidate) { return a.isSatisfiedBy(candidate) && b.isSatisfiedBy(candidate); }
}
使用示例:
Specification spec =
new NotEmptySpecification()
.and(new ContainsWordSpecification("Hello").or(new ContainsWordSpecification("Hi")))
.and(new LengthLessThanSpecification(100));
// 然后过滤列表:stream.filter(spec::isSatisfiedBy)
C# 实现要点
C# 的一个优势是 表达树(Expression),可以把规格转换为 LINQ 可识别的表达式,直接用于数据库查询(例如 EF)。基本接口:
public interface ISpecification {
bool IsSatisfiedBy(T candidate);
Expression> ToExpression();
}
实现思路:
- 在 IsSatisfiedBy 中调用 ToExpression().Compile()(candidate)(仅用于内存判断);
- 组合时合并表达树:Expression.AndAlso / OrElse;
- 这样合成的表达式可直接传给 IQueryable.Where,避免把数据拉到内存。
示例(伪代码):
public class NotEmptySpec : ISpecification {
public Expression> ToExpression() => s => !string.IsNullOrWhiteSpace(s);
}
JavaScript(Node / 前端)实现
JavaScript 里没有表达树,但用函数式很自然:规格就是返回布尔的函数或对象。接口可以简单:
class Specification {
isSatisfiedBy(candidate) { throw 'not implemented'; }
and(other) { return new AndSpec(this, other); }
or(other) { return new OrSpec(this, other); }
not() { return new NotSpec(this); }
}
或者更简洁地,规格就是 (candidate) => boolean 的函数,再用组合函数组合:
const notEmpty = s => s && s.trim().length > 0;
const contains = word => s => s && s.includes(word);
const and = (a,b) => s => a(s) && b(s);
这在前端和小型后端服务里特别实用,简单、直观,但如果需要把过滤下推到数据库,就不够用了。
单元测试与边界条件
规格模式的一个大好处是易于测试。测试建议:
- 对每个基本规格做单元测试,覆盖阳性和阴性案例;
- 对组合规格测试组合逻辑是否正确(优先级、短路行为);
- 针对边界情况(null、空串、极长字符串、包含特殊字符)编写测试;
- 如果使用表达树或下推到数据库,要写集成测试验证查询行为一致。
举个具体例子:测试 ContainsWordSpecification:
- 输入 “Hello World” 应返回 true;
- 输入 “hello”(大小写不同)根据实现可能为 false,需明确规范是否区分大小写;
- 输入 null 应返回 false。
性能与工程化建议
- 避免在热路径频繁分配对象:如果规格对象很小可以重用实例;
- 表达式下推:在 C# 中把规格转换为 Expression 可下推给 ORM,避免把数据全部拉到内存;
- 缓存组合结果:复杂组合可能重复执行,考虑 memoization 或把规格编译成高效 predicate;
- 日志与可观测:在复杂规则链上打点或记录命中原因,便于排查“为什么某条数据被过滤掉”;
- 文档化规则:把每个规格的语义写清楚(是否空格敏感、是否区分大小写等)。
常见陷阱与变体
- 规格过细导致数量爆炸:把一堆小规格组合虽灵活,但维护成本上升。适当合并相关规则,保持概念清晰。
- 规格与领域服务混淆:规格应该只做判断,不应承担修改状态或副作用。
- 在分布式场景下的限制:把规格以代码形式存在很好,但在跨服务或非统一语言环境中不可重用,这时考虑把规则转为配置或 DSL。
- 表达式语义丢失:把规格转为 SQL 或 ORM 表达式时,注意某些语言特性(例如正则)在目标存储中可能不支持或语义不同。
对比表(快速参考)
| 语言 | 组合方式 | 是否支持表达式下推 | 适用场景 |
| Java | 接口 + 组合类 / lambda | 有限(需要框架支持,如 QueryDSL) | 后端逻辑规则、服务内过滤 |
| C# | 接口 + Expression 合并 | 良好(LINQ/EF 可下推) | 需要与数据库查询集成的场景 |
| JavaScript | 函数式或类实现 | 否(需手动转换为查询语言) | 前端校验、小型后端服务 |
最佳实践(几条实战建议)
- 把规格设计成小而明确的单元,名词化表达规则意图;
- 为规格提供清晰的文档,明确边界(null、空串、大小写);
- 如果需要数据库下推,从一开始就考虑能否转成表达式;
- 对经常组合的规则提供预定义组合,减少重复代码;
- 在规则变化频繁的领域,评估用配置或 DSL 替代硬编码规格。
把 HelloWorld 做成真实项目的一些小建议
别把示例写死在业务逻辑里。即便是 HelloWorld 这样的练手项目,也可以从一开始把规格抽离到单独模块,写好接口和测试,这样当规则增长成几十条时,修改点集中、回归测试清晰。还有一点:在团队里统一命名和语义(比如 allCaps、ignoreCase),免得大家用法不一致导致 bug。
参考读物(便于深入)
- Domain-Driven Design — Eric Evans(对领域规则建模有帮助)
- Patterns of Enterprise Application Architecture — Martin Fowler(包含设计模式背景)
- 相关社区文章与 ORM 文档(例如 EF、Hibernate)用于理解表达式下推细节
好啦,想法说到这儿就比较清楚了。实现时你会发现,从简单的 HelloWorld 练起能迅速体会到规格模式在测试性、组合性和可维护性上的好处,接下来就是把这些原则按需扩展到实际业务里,别忘了把测试和文档一并做好,省得日后找坑。