1. 过滤器模式初探:当筛选逻辑变得复杂时
去年重构一个电商平台的商品筛选系统时,我遇到了一个典型问题:随着业务发展,筛选条件从最初简单的价格区间扩展到了品牌、销量、评价、促销类型等十余个维度。原先硬编码的if-else链已经膨胀到近千行,每次新增条件都需要修改核心逻辑。这正是过滤器模式(Filter Pattern)大显身手的场景。
过滤器模式属于结构型设计模式,它通过定义一组可插拔的过滤规则,将筛选条件与核心业务逻辑解耦。想象一下现实中的筛子——我们可以按颗粒大小、材质等不同标准叠加使用多个筛子,而过滤器模式正是这种思想的编程实现。在Java中,它通常表现为一个标准接口(Criteria)、多个具体实现类,以及一个组合这些过滤器的容器类。
提示:当你的系统中存在以下特征时,就该考虑过滤器模式了:筛选条件频繁变化、多条件需要灵活组合、条件之间存在复杂逻辑关系。
2. 模式结构与核心组件拆解
2.1 标准接口设计
过滤器模式的核心在于定义一个统一的过滤接口。以用户筛选为例:
java复制public interface Criteria<T> {
List<T> meetCriteria(List<T> items);
}
这个简单的接口却蕴含着强大扩展性。任何实现了meetCriteria方法的类都可以成为过滤器,比如下面这个按年龄过滤的实现:
java复制public class AgeCriteria implements Criteria<User> {
private final int minAge;
public AgeCriteria(int minAge) {
this.minAge = minAge;
}
@Override
public List<User> meetCriteria(List<User> users) {
return users.stream()
.filter(u -> u.getAge() >= minAge)
.collect(Collectors.toList());
}
}
2.2 组合过滤器的实现
真正的威力来自于过滤器的组合。我们可以创建AndCriteria、OrCriteria等逻辑组合器:
java复制public class AndCriteria<T> implements Criteria<T> {
private final Criteria<T> first;
private final Criteria<T> second;
public AndCriteria(Criteria<T> first, Criteria<T> second) {
this.first = first;
this.second = second;
}
@Override
public List<T> meetCriteria(List<T> items) {
return second.meetCriteria(first.meetCriteria(items));
}
}
这种设计允许我们像搭积木一样构建复杂筛选逻辑。例如要筛选"年龄大于18且来自北京的用户",只需:
java复制Criteria<User> criteria = new AndCriteria<>(
new AgeCriteria(18),
new CityCriteria("北京")
);
2.3 与策略模式的区别
初学者常混淆过滤器模式与策略模式。虽然都涉及接口和实现类,但它们的关注点不同:
| 特性 | 过滤器模式 | 策略模式 |
|---|---|---|
| 目的 | 筛选符合条件的对象 | 封装可互换的算法 |
| 输入输出 | 集合→子集 | 参数→结果 |
| 典型应用 | 数据过滤、搜索条件 | 支付方式、排序算法 |
| 组合方式 | 常通过逻辑运算符组合 | 通常独立使用 |
3. 实战:电商商品筛选系统改造
3.1 原始代码的问题
假设原有商品筛选代码如下:
java复制public List<Product> filterProducts(List<Product> products, FilterParams params) {
List<Product> result = new ArrayList<>();
for (Product p : products) {
if (params.getMinPrice() != null && p.getPrice() < params.getMinPrice()) {
continue;
}
if (params.getMaxPrice() != null && p.getPrice() > params.getMaxPrice()) {
continue;
}
// 后续还有10余个类似条件判断...
result.add(p);
}
return result;
}
这种硬编码方式存在几个明显问题:
- 方法随着条件增加不断膨胀
- 修改任一条件都需要改动核心方法
- 难以实现动态条件组合
- 单元测试覆盖所有条件组合几乎不可能
3.2 重构为过滤器模式
首先定义商品过滤器接口:
java复制public interface ProductCriteria {
List<Product> filter(List<Product> products);
}
然后实现各种具体条件:
java复制public class PriceRangeCriteria implements ProductCriteria {
private final BigDecimal min;
private final BigDecimal max;
// 构造器和getter省略
@Override
public List<Product> filter(List<Product> products) {
return products.stream()
.filter(p -> min == null || p.getPrice().compareTo(min) >= 0)
.filter(p -> max == null || p.getPrice().compareTo(max) <= 0)
.collect(Collectors.toList());
}
}
组合使用时可以这样:
java复制List<ProductCriteria> criteriaList = new ArrayList<>();
if (params.getMinPrice() != null || params.getMaxPrice() != null) {
criteriaList.add(new PriceRangeCriteria(params.getMinPrice(), params.getMaxPrice()));
}
// 添加其他条件...
ProductCriteria finalCriteria = criteriaList.stream()
.reduce(c -> c, (c1, c2) -> products -> c2.filter(c1.filter(products)));
return finalCriteria.filter(products);
3.3 性能优化技巧
过滤器模式虽然优雅,但在大数据量下可能产生性能问题。以下是几个优化方向:
-
短路评估:在组合过滤器时,优先执行高过滤率的条件。例如先按分类过滤再按价格,可以减少后续操作的数据量。
-
并行处理:对于CPU密集型的过滤条件,可以使用并行流:
java复制return products.parallelStream() .filter(/*条件*/) .collect(Collectors.toList()); -
缓存机制:对不变的数据集,可以缓存过滤结果。Guava的LoadingCache是不错的选择。
-
批量操作:避免在循环中创建大量临时对象,尽量使用原生类型和批量操作。
4. 高级应用与模式变体
4.1 动态条件组装
结合反射和配置化,可以实现动态过滤器组装。例如从JSON配置生成过滤器链:
json复制{
"condition": "AND",
"rules": [
{
"field": "price",
"operator": "between",
"value": [100, 500]
},
{
"condition": "OR",
"rules": [
{"field": "category", "operator": "eq", "value": "电子"},
{"field": "sales", "operator": "gt", "value": 1000}
]
}
]
}
对应的解析器可以动态创建Criteria实例,这种技术在规则引擎中很常见。
4.2 函数式编程实现
Java 8以后,我们可以用Predicate和函数组合实现更简洁的过滤器:
java复制Predicate<Product> priceFilter = p ->
params.getMinPrice() == null || p.getPrice() >= params.getMinPrice();
Predicate<Product> categoryFilter = p ->
params.getCategory() == null || p.getCategory().equals(params.getCategory());
Predicate<Product> finalFilter = priceFilter.and(categoryFilter);
return products.stream()
.filter(finalFilter)
.collect(Collectors.toList());
4.3 与Spring框架集成
在Spring应用中,可以优雅地管理过滤器bean:
java复制@Bean
@Qualifier("electronicFilter")
public ProductCriteria electronicFilter() {
return products -> products.stream()
.filter(p -> "电子".equals(p.getCategory()))
.collect(Collectors.toList());
}
@Bean
public ProductCriteria hotSaleFilter() {
return products -> products.stream()
.filter(p -> p.getSales() > 1000)
.collect(Collectors.toList());
}
使用时通过@Autowired注入特定过滤器,或者用@Qualifier指定组合方式。
5. 常见陷阱与最佳实践
5.1 易犯的错误
-
过度设计:简单条件直接使用Stream.filter可能更合适,不必强行套用模式。
-
忽略线程安全:如果过滤器包含状态(如计数器),需要确保线程安全。
-
性能盲区:链式调用会产生中间集合,大数据量时可能内存吃紧。
-
空值处理不当:没有妥善处理null集合或null元素条件。
5.2 测试策略
过滤器模式的一个优势是便于单元测试。测试要点包括:
java复制@Test
void testPriceFilter() {
PriceRangeCriteria criteria = new PriceRangeCriteria(new BigDecimal("100"), null);
List<Product> products = Arrays.asList(
new Product("A", new BigDecimal("80")),
new Product("B", new BigDecimal("120"))
);
List<Product> result = criteria.filter(products);
assertEquals(1, result.size());
assertEquals("B", result.get(0).getName());
}
@Test
void testCombinedFilter() {
ProductCriteria criteria = new AndCriteria<>(
new PriceRangeCriteria(new BigDecimal("100"), new BigDecimal("500")),
new CategoryCriteria("电子")
);
// 测试数据准备和断言
}
5.3 日志与监控
在生产环境中,建议为过滤器添加监控点:
java复制public List<Product> filter(List<Product> products) {
long start = System.currentTimeMillis();
try {
// 实际过滤逻辑
} finally {
log.debug("Filter {} executed in {}ms",
this.getClass().getSimpleName(),
System.currentTimeMillis() - start);
}
}
这有助于识别性能瓶颈,特别是在复杂过滤器链中。
6. 模式扩展与行业应用
6.1 与其他模式的协作
过滤器模式常与其他模式配合使用:
- 装饰器模式:为过滤器添加额外功能,如日志、缓存等
- 责任链模式:将过滤器组织为处理链条
- 组合模式:构建复杂的嵌套过滤条件树
6.2 行业应用案例
-
电商系统:
- 商品多维度筛选
- 促销活动资格判定
- 搜索结果的二次过滤
-
金融领域:
- 风险交易识别
- 客户分群筛选
- 投资组合过滤
-
游戏开发:
- 场景实体可见性过滤
- 碰撞检测预筛选
- AI决策条件判断
-
大数据处理:
- MapReduce中的过滤阶段
- 流式数据的实时过滤
- ETL过程中的数据清洗
在最近参与的一个风控系统中,我们使用过滤器模式实现了规则引擎的核心过滤模块。通过动态加载过滤器配置,业务人员可以在不重启系统的情况下调整风险规则,将规则变更周期从原来的2周缩短到实时生效。