规格模式(Specification):把业务规则变成乐高积木

说实话,我第一次接触规格模式是在一个电商后台项目里。当时有个需求——查询「可参与满减活动的商品」,条件多得让人头大:商品要上架、库存要大于0、价格要超过门槛、不能是黑名单商品、用户等级要达标……if 嵌套了七八层,看得我直摇头。

后来我重构了那段代码,用的就是规格模式。说白了,就是把每个判断条件封装成一个独立的「规格」,然后用 AND、OR、NOT 这些逻辑运算符把它们组合起来。你想想看,这不就是业务逻辑的乐高积木吗?

什么是规格模式?

规格模式的核心思想很简单:把业务规则抽象成接口,每个规则是一个独立的对象,然后通过组合器把它们拼装起来。这样做的最大好处是——业务变化时,你只需要增删规格,不用动核心逻辑。

我个人的习惯是,只要发现业务条件超过 3 个,而且未来可能变化,就优先考虑规格模式。它特别适合做查询条件动态组装、权限校验、工作流规则判断这些场景。

核心角色

角色 说明
规格接口(ISpecification) 定义判断方法,以及 AND、OR、NOT 组合操作
具体规格(ConcreteSpecification) 实现单个业务规则,比如「价格大于100」
复合规格(CompositeSpecification) 负责 AND、OR、NOT 的逻辑组合
客户端 组装规格并执行判断或过滤

先看接口定义

嗯,这里要注意:接口里除了 isSatisfiedBy 方法,还要提供组合方法。我个人喜欢用抽象类来实现默认的组合逻辑,这样具体规格就不用重复写了。

// Java 版本
public interface ISpecification<T> {
    boolean isSatisfiedBy(T candidate);
    ISpecification<T> and(ISpecification<T> other);
    ISpecification<T> or(ISpecification<T> other);
    ISpecification<T> not();
}

public abstract class CompositeSpecification<T> implements ISpecification<T> {
    @Override
    public ISpecification<T> and(ISpecification<T> other) {
        return new AndSpecification<>(this, other);
    }
    @Override
    public ISpecification<T> or(ISpecification<T> other) {
        return new OrSpecification<>(this, other);
    }
    @Override
    public ISpecification<T> not() {
        return new NotSpecification<>(this);
    }
}
// C++ 版本
template <typename T>
class ISpecification {
public:
    virtual ~ISpecification() = default;
    virtual bool isSatisfiedBy(const T& candidate) const = 0;
    virtual std::shared_ptr<ISpecification<T>> and_(std::shared_ptr<ISpecification<T>> other) const;
    virtual std::shared_ptr<ISpecification<T>> or_(std::shared_ptr<ISpecification<T>> other) const;
    virtual std::shared_ptr<ISpecification<T>> not_() const;
};

复合规格的实现

复合规格说白了就是逻辑运算的封装。我曾经见过有人直接在业务代码里写 if (a && b || !c),后来条件一变,改得满头大汗。用复合规格,每个逻辑运算都是一个类,清晰又好测。

// Java 版本
public class AndSpecification<T> extends CompositeSpecification<T> {
    private ISpecification<T> left;
    private ISpecification<T> right;
    
    public AndSpecification(ISpecification<T> left, ISpecification<T> right) {
        this.left = left;
        this.right = right;
    }
    
    @Override
    public boolean isSatisfiedBy(T candidate) {
        return left.isSatisfiedBy(candidate) && right.isSatisfiedBy(candidate);
    }
}

public class OrSpecification<T> extends CompositeSpecification<T> {
    // 类似 AndSpecification,用 || 连接
}

public class NotSpecification<T> extends CompositeSpecification<T> {
    private ISpecification<T> spec;
    
    public NotSpecification(ISpecification<T> spec) {
        this.spec = spec;
    }
    
    @Override
    public boolean isSatisfiedBy(T candidate) {
        return !spec.isSatisfiedBy(candidate);
    }
}

具体规格:把业务规则写清楚

每个具体规格只做一件事。比如「价格大于100」就是一个规格,「库存大于0」是另一个规格。这样每个规格都可以单独测试,出了问题也容易定位。

// Java 版本
public class PriceGreaterThanSpec extends CompositeSpecification<Product> {
    private double threshold;
    
    public PriceGreaterThanSpec(double threshold) {
        this.threshold = threshold;
    }
    
    @Override
    public boolean isSatisfiedBy(Product product) {
        return product.getPrice() > threshold;
    }
}

public class InStockSpec extends CompositeSpecification<Product> {
    @Override
    public boolean isSatisfiedBy(Product product) {
        return product.getStock() > 0;
    }
}

public class CategorySpec extends CompositeSpecification<Product> {
    private String category;
    
    public CategorySpec(String category) {
        this.category = category;
    }
    
    @Override
    public boolean isSatisfiedBy(Product product) {
        return category.equals(product.getCategory());
    }
}

动态组装查询条件

这才是规格模式最爽的地方。你想想看,前端传过来一堆筛选条件,后端不用写一长串 if-else,直接组装规格就行。

// Java 版本:动态组装
public List<Product> queryProducts(ProductQuery query) {
    ISpecification<Product> spec = new AlwaysTrueSpec<>();
    
    if (query.getMinPrice() != null) {
        spec = spec.and(new PriceGreaterThanSpec(query.getMinPrice()));
    }
    if (query.getCategory() != null) {
        spec = spec.and(new CategorySpec(query.getCategory()));
    }
    if (query.getInStockOnly()) {
        spec = spec.and(new InStockSpec());
    }
    
    // 还可以排除某些品类
    if (query.getExcludeCategory() != null) {
        spec = spec.and(new NotSpecification<>(new CategorySpec(query.getExcludeCategory())));
    }
    
    return productRepository.findAll().stream()
            .filter(spec::isSatisfiedBy)
            .collect(Collectors.toList());
}

核心要点:规格模式把「判断逻辑」和「组合逻辑」彻底分离。每个规格只关心自己那一个条件,组合方式由客户端决定。这样业务规则的变化,就变成了规格的增删改,而不是大段代码的重写。

SVG 结构图:规格模式的核心逻辑

规格模式核心结构 ISpecification<T> CompositeSpecification PriceGreaterThanSpec InStockSpec CategorySpec AndSpecification OrSpecification NotSpecification 接口 抽象类 具体规格 复合规格

避坑指南

我曾经踩过的一个坑:一开始我把所有规格都设计成单例,想着反正逻辑一样。结果有个规格需要根据用户等级动态调整阈值,单例就出问题了——多个请求互相覆盖参数。后来我改成每次组装时 new 新的规格实例,问题就解决了。

我的建议:规格模式虽然好用,但别滥用。如果只有一两个条件,直接写 if 反而更简洁。我一般会在条件超过 3 个,或者业务规则经常变化时,才引入规格模式。另外,配合工厂模式使用效果更佳——把规格的创建集中管理,客户端只需要传参数就行。

实际项目中的应用

我记得在一个权限系统中,我们用规格模式实现了动态的权限校验。每个权限规则是一个规格,比如「用户属于管理员组」、「操作时间在 9:00-18:00」、「IP 在白名单内」。然后通过 AND/OR 组合出复杂的权限策略。后来业务方要加一个新规则,我们只需要新增一个规格类,然后在配置中心里改一下组合方式,完全不用动核心代码。

说白了,规格模式就是让业务规则变得可组合、可扩展、可测试。你想想看,如果每个规则都是独立的小单元,改一个不会影响另一个,测试也能单独测,多省心。


公众号:蓝海资料掘金营,微信deep3321