HelloWorld 规格模式教程

2026年7月23日 作者:admin

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

HelloWorld 规格模式教程

先把概念说清楚:规格模式到底是什么?

简单来说,规格模式(Specification Pattern)把“是否满足某个业务条件”抽象为一个对象,它通常提供一个方法(比如 isSatisfiedBy)来判断某个候选对象是否满足该条件。把规则封成对象后,你可以:

  • 把规则像搭积木一样组合(并且、或者、非);
  • 把规则独立测试;
  • 在需要时把规则转换为查询表达式(比如数据库查询或流过滤器)。

用费曼法再解释一遍:想象你在家里检查“物品是否可以带上飞机”的规则。每条规则是一个检查项,比如“液体小于100ml”“电池装好”“没有危险品”。把每条检查项做成一张小卡片(规格),你可以单独验证某张卡片,也可以把几张卡片合成一套检查流程。规格模式就是把编程里的“检查项”做成卡片。

规格的核心要素

接口与实现

核心通常只有一个接口/抽象类,约定一个判断方法,例如:

  • isSatisfiedBy(candidate):直接对对象做布尔判断。
  • 有时会提供 toPredicatetoExpression,用于把规格转成可组合的查询表达式(便于数据库或框架优化)。

组合算子

常见的组合算子是 ANDORNOT。实现方式通常是把两个或多个规格包装成组合规格,组合规格内部会调用子规格的 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 练起能迅速体会到规格模式在测试性、组合性和可维护性上的好处,接下来就是把这些原则按需扩展到实际业务里,别忘了把测试和文档一并做好,省得日后找坑。

相关文章

了解更多相关内容

HelloWorld智能翻译软件 与世界各地高效连接