[设计模式/Java] 设计模式之过滤器模式【15】

概述:管道/过滤器模式 := Pipeline/Filter Pattern := 标准模式

模式简介

  • 过滤器模式(Filter Pattern)或标准模式(Criteria Pattern)是一种设计模式
  • 这种模式允许开发人员使用不同的标准过滤一组对象,通过逻辑运算解耦的方式把它们连接起来。
  • 这种类型的设计模式属于结构型模式,它结合多个标准来获得单一标准
  • 管道/过滤器模式体系结构面向数据流的软件体系结构。

管道/过滤器模式中,每个组件(过滤器)都有一组输入/输出, 组件读取输入的数据流,经过内部处理后,产生输出的数据流,该过程主要完成输入流的变换及增量计算
该模式如下图所示:

  • 本模式最典型的应用编译系统批处理系统、Java Web Filter(javax.servlet.Filter/@WebFilter) / Interceptor等。
  • 一个普通的编译系统包括词法分析器,语法分析器,语义分析与中间代码生成器,代码优化器,目标代码生成器等一系列对源程序进行处理的过程。

人们可以将编译系统看作一系列过滤器的连接体,按照管道/过滤器的体系结构进行设计。
此外,这种体系结构在其它一些领域也有广泛的应用。
因此,它成为软件工程和软件开发中的一个突出的研究领域。

  • 相关文献

  • 模式意图
  • 用于将对象的筛选过程封装起来,允许使用不同的筛选标准动态地筛选对象。
  • 主要解决的问题
  • 当需要根据多个不同的条件或标准来筛选一组对象时,过滤器模式提供了一种灵活的方式来定义这些条件,避免在客户端代码中硬编码筛选逻辑。

适用场景

  • 当对象集合需要根据不同的标准进行筛选时。
  • 当筛选逻辑可能变化,或者需要动态地组合多个筛选条件时。

模式的组成

  • 过滤器接口(Filter/Criteria):定义一个接口,用于筛选对象。该接口通常包含一个方法,用于根据特定条件过滤对象。

  • 具体过滤器类(Concrete Filter/Concrete Criteria):实现过滤器接口,具体定义筛选对象的条件和逻辑。

  • 对象集合(Items/Objects to be filtered):要被过滤的对象集合。这些对象通常是具有共同属性的实例,例如一组人、一组产品等。

  • 客户端(Client):使用具体过滤器类来筛选对象集合。客户端将对象集合和过滤器结合起来,以获得符合条件的对象。

实现方式

  • 定义筛选接口:创建一个筛选接口,定义一个筛选方法。
  • 实现具体筛选器:为每个筛选标准实现筛选接口,封装具体的筛选逻辑。
  • 组合筛选器:允许筛选器之间进行组合,形成复杂的筛选逻辑。

关键代码

  • 筛选接口:定义筛选方法,如 matches()。
  • 具体筛选器类:实现筛选接口,封装具体的筛选逻辑。
  • 组合筛选器:实现筛选器的组合逻辑,如逻辑与(AND)、逻辑或(OR)等。

模式特点

优点

  • 封装性:筛选逻辑被封装在独立的筛选器对象中。
  • 灵活性:可以动态地添加、修改或组合筛选条件。
  • 可扩展性:容易添加新的筛选标准,无需修改现有代码。

缺点

  • 复杂性:随着筛选条件的增加,系统可能变得复杂。
  • 性能问题:如果筛选器组合过于复杂,可能会影响性能。

注意事项

  • 当筛选逻辑可能变化或需要根据不同标准动态筛选对象时,考虑使用过滤器模式。
  • 在设计时,确保筛选器的接口和实现保持一致,以便于组合和扩展。
  • 确保筛选器的组合逻辑正确无误,避免引入逻辑错误。
  • 在实现时,考虑性能影响,特别是在处理大量数据时。

案例实践

CASE: 图书管理系统:根据作者、出版年份、类别等不同标准筛选图书

CASE: 在线购物平台:根据价格、品牌、用户评分等条件筛选商品

CASE: 过滤人员信息

我们将创建一个 Person 对象、Criteria 接口和实现了该接口的实体类,来过滤 Person 对象的列表。CriteriaPatternDemo 类使用 Criteria 对象,基于各种标准和它们的结合来过滤 Person 对象的列表。

Person : 被应用标准/过滤器的类

  • 创建一个类,在该类上应用标准。
public class Person {
   
   private String name;
   private String gender;
   private String maritalStatus;
 
   public Person(String name,String gender,String maritalStatus){
      this.name = name;
      this.gender = gender;
      this.maritalStatus = maritalStatus;    
   }
 
   public String getName() {
      return name;
   }
   public String getGender() {
      return gender;
   }
   public String getMaritalStatus() {
      return maritalStatus;
   }  
}

Criteria

  • 为标准(Criteria)创建一个接口
  • Criteria
import java.util.List;
 
public interface Criteria {
   public List<Person> meetCriteria(List<Person> persons);
}

CriteriaMale / CriteriaFemale / CriteriaSingle : 实现标准/过滤器的类

  • 创建实现了 Criteria 接口的实体类
  • CriteriaMale
import java.util.ArrayList;
import java.util.List;
 
public class CriteriaMale implements Criteria {
 
   @Override
   public List<Person> meetCriteria(List<Person> persons) {
      List<Person> malePersons = new ArrayList<Person>(); 
      for (Person person : persons) {
         if(person.getGender().equalsIgnoreCase("MALE")){
            malePersons.add(person);
         }
      }
      return malePersons;
   }
}
  • CriteriaFemale
import java.util.ArrayList;
import java.util.List;
 
public class CriteriaFemale implements Criteria {
   @Override
   public List<Person> meetCriteria(List<Person> persons) {
      List<Person> femalePersons = new ArrayList<Person>(); 
      for (Person person : persons) {
         if(person.getGender().equalsIgnoreCase("FEMALE")){
            femalePersons.add(person);
         }
      }
      return femalePersons;
   }
}
  • CriteriaSingle
import java.util.ArrayList;
import java.util.List;
 
public class CriteriaSingle implements Criteria {
 
   @Override
   public List<Person> meetCriteria(List<Person> persons) {
      List<Person> singlePersons = new ArrayList<Person>(); 
      for (Person person : persons) {
         if(person.getMaritalStatus().equalsIgnoreCase("SINGLE")){
            singlePersons.add(person);
         }
      }
      return singlePersons;
   }
}

AndCriteria / OrCriteria : 逻辑Filter实现

  • AndCriteria
import java.util.List;
 
public class AndCriteria implements Criteria {
 
   private Criteria criteria;
   private Criteria otherCriteria;
 
   public AndCriteria(Criteria criteria, Criteria otherCriteria) {
      this.criteria = criteria;
      this.otherCriteria = otherCriteria; 
   }
 
   @Override
   public List<Person> meetCriteria(List<Person> persons) {
      List<Person> firstCriteriaPersons = criteria.meetCriteria(persons);     
      return otherCriteria.meetCriteria(firstCriteriaPersons);
   }
}
  • OrCriteria
import java.util.List;
 
public class OrCriteria implements Criteria {
 
   private Criteria criteria;
   private Criteria otherCriteria;
 
   public OrCriteria(Criteria criteria, Criteria otherCriteria) {
      this.criteria = criteria;
      this.otherCriteria = otherCriteria; 
   }
 
   @Override
   public List<Person> meetCriteria(List<Person> persons) {
      List<Person> firstCriteriaItems = criteria.meetCriteria(persons);
      List<Person> otherCriteriaItems = otherCriteria.meetCriteria(persons);
 
      for (Person person : otherCriteriaItems) {
         if(!firstCriteriaItems.contains(person)){
           firstCriteriaItems.add(person);
         }
      }  
      return firstCriteriaItems;
   }
}

Client

  • 使用不同的标准(Criteria)和它们的结合来过滤 Person 对象的列表。
import java.util.ArrayList; 
import java.util.List;
 
public class CriteriaPatternDemo {
   public static void main(String[] args) {
      List<Person> persons = new ArrayList<Person>();
 
      persons.add(new Person("Robert","Male", "Single"));
      persons.add(new Person("John","Male", "Married"));
      persons.add(new Person("Laura","Female", "Married"));
      persons.add(new Person("Diana","Female", "Single"));
      persons.add(new Person("Mike","Male", "Single"));
      persons.add(new Person("Bobby","Male", "Single"));
 
      Criteria male = new CriteriaMale();
      Criteria female = new CriteriaFemale();
      Criteria single = new CriteriaSingle();
      Criteria singleMale = new AndCriteria(single, male);
      Criteria singleOrFemale = new OrCriteria(single, female);
 
      System.out.println("Males: ");
      printPersons(male.meetCriteria(persons));
 
      System.out.println("\nFemales: ");
      printPersons(female.meetCriteria(persons));
 
      System.out.println("\nSingle Males: ");
      printPersons(singleMale.meetCriteria(persons));
 
      System.out.println("\nSingle Or Females: ");
      printPersons(singleOrFemale.meetCriteria(persons));
   }
 
   public static void printPersons(List<Person> persons){
      for (Person person : persons) {
         System.out.println("Person : [ Name : " + person.getName() 
            +", Gender : " + person.getGender() 
            +", Marital Status : " + person.getMaritalStatus()
            +" ]");
      }
   }      
}

out

Males: 
Person : [ Name : Robert, Gender : Male, Marital Status : Single ]
Person : [ Name : John, Gender : Male, Marital Status : Married ]
Person : [ Name : Mike, Gender : Male, Marital Status : Single ]
Person : [ Name : Bobby, Gender : Male, Marital Status : Single ]

Females: 
Person : [ Name : Laura, Gender : Female, Marital Status : Married ]
Person : [ Name : Diana, Gender : Female, Marital Status : Single ]

Single Males: 
Person : [ Name : Robert, Gender : Male, Marital Status : Single ]
Person : [ Name : Mike, Gender : Male, Marital Status : Single ]
Person : [ Name : Bobby, Gender : Male, Marital Status : Single ]

Single Or Females: 
Person : [ Name : Robert, Gender : Male, Marital Status : Single ]
Person : [ Name : Diana, Gender : Female, Marital Status : Single ]
Person : [ Name : Mike, Gender : Male, Marital Status : Single ]
Person : [ Name : Bobby, Gender : Male, Marital Status : Single ]
Person : [ Name : Laura, Gender : Female, Marital Status : Married ]

CASE: 多条件过滤/查询书籍

  • 在有些场景中,需要对一个集合的对象进行过滤。

比如,我有很多本书,想要知道价格为50元以上且出版社包含工业字样的书籍有哪些

Book


import java.io.Serializable;
import java.util.List;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.ToString;

@Data
@AllArgsConstructor
@ToString
public class Book implements Serializable {

  private static final long serialVersionUID = -6212470156629515269L;

  /**
   * 书名
   */
  private String name;

  /**
   * 价格
   */
  private double price;

  /**
   * 作者
   */
  private List<String> authors;

  /**
   * 出版社
   */
  private String press;
}

传统的过滤做法(硬编码)

  • 简单过滤的基本逻辑:

for (Book elem : books) {
    // 价格大于50元 且 出版社含有'工业'字样
    if (elem.getPrice() > 50 && elem.getPress().contains("工业")) {
	    filterBooks.add(elem);
    }
}
  • 示例

import java.util.Collections;
import java.util.List;

import org.springframework.util.CollectionUtils;

import com.alibaba.fastjson.JSON;
import com.google.common.collect.Lists;

import lombok.extern.slf4j.Slf4j;

@Slf4j
public class BookFilterMain {

  public static void main(String[] args) {
    List<Book> originBooks = prepareData();
    List<Book> filterBooks = filter1(originBooks);
    log.info("传统方式-过滤后结果 ==> {}", JSON.toJSONString(filterBooks, true));
  }

  private static List<Book> prepareData() {

    Book book1 = new Book("微服务架构设计模式", 139.00, Lists.newArrayList("克里斯"), "机械工业出版社");
    Book book2 = new Book("未来简史", 79.00, Lists.newArrayList("王骥"), "电子工业出版社");
    Book book3 = new Book("Java核心", 99.00, Lists.newArrayList("张三", "李四", "王五"), "人民邮电出版社");
    return Lists.newArrayList(book1, book2, book3);
  }

  private static List<Book> filter1(List<Book> books) {
    if (CollectionUtils.isEmpty(books)) {
      return Collections.emptyList();
    }

    List<Book> filterBooks = Lists.newArrayList();
    for (Book elem : books) {
      // 价格大于50元 且 出版社含有'工业'字样
      if (elem.getPrice() > 50 && elem.getPress().contains("工业")) {
        filterBooks.add(elem);
      }
    }
    return filterBooks;
  }
}
  • out

由此,我们找到了符合过滤条件的书籍列表

16:17:43.571 [main] INFO BookFilterMain - 传统方式-过滤后结果 ==> [
  {
    "authors":[
      "克里斯"
    ],
    "name":"微服务架构设计模式",
    "press":"机械工业出版社",
    "price":139.0
  },
  {
    "authors":[
      "王骥"
    ],
    "name":"未来简史",
    "press":"电子工业出版社",
    "price":79.0
  }
]

Filter 模式(第1种实现方式)

继续以上述Book为例,大致的结构如下图所示

Filter 接口


import java.util.List;

public interface Filter {
    List<Book> apply(List<Book> books);
}

PriceGreaterFilter / ... : 具体Filter实现

PriceGreaterFilter
  • PriceGreaterFilter : 如 价格大于50元

import java.util.Collections;
import java.util.List;

import org.springframework.util.CollectionUtils;

import com.google.common.collect.Lists;

public class PriceGreaterFilter implements Filter {

  private double price;

  public PriceGreaterFilter(double price) {
    this.price = price;
  }

  @Override
  public List<Book> apply(List<Book> books) {
    if (CollectionUtils.isEmpty(books)) {
      return Collections.emptyList();
    }
    List<Book> filterBooks = Lists.newArrayList();
    for (Book elem : books) {
      if (elem.getPrice() > price) {
        filterBooks.add(elem);
      }
    }
    return filterBooks;
  }
}
PressContainFilter
  • PressContainFilter :如 出版社包含工业字样
import java.util.Collections;
import java.util.List;

import org.springframework.util.CollectionUtils;

import com.google.common.collect.Lists;

public class PressContainFilter implements Filter {
  
  private String name;

  public PressContainFilter(String name) {
    this.name = name;
  }

  @Override
  public List<Book> apply(List<Book> books) {
    if(CollectionUtils.isEmpty(books)) {
      return Collections.emptyList();
    }
     List<Book> filterBooks = Lists.newArrayList();
     for(Book elem : books) {
       if(elem.getPress().contains(this.name)) {
         filterBooks.add(elem);
       }
     }
    return filterBooks;
  }
}

AndFilter / OrFilter:逻辑Filter实现

  • AndFilter
import java.util.List;

public class AndFilter implements Filter {

  private Filter first;;

  private Filter second;

  public AndFilter(Filter first, Filter second) {
    this.first = first;
    this.second = second;
  }

  @Override
  public List<Book> apply(List<Book> books) {
    List<Book> firstFilterBooks = first.apply(books);
    List<Book> secondFilterBooks = second.apply(firstFilterBooks);
    return secondFilterBooks;
  }
}

Client

这样,我们就能通过如下方式完成书本的过滤,找到想要的内容。

// 创建Filter
Filter filter = new AndFilter(new PriceGreaterFilter(50), new PressContainFilter("工业"));
//获取过滤后的结果
List<Book> newFilterBooks = filter.apply(originBooks);

上述过滤器的实现方式,其实现了过滤条件的解耦,但每个具体的过滤器都要遍历一次列表集合,还是比较消耗性能的。

其实,我们只要一次for循环,对循环中的对象按照组合条件的过滤器进行过滤即可。
这样,Filter接口只要一个Book对象,而不需要一个集合,如List。

Filter 模式(第2种实现方式)

接下来,我们来完成第二个实现方式。

Filter 接口

public interface FilterV2 {
  boolean apply(Book book);
}

具体Filter实现

PriceGreaterFilterV2
  • 价格大于50元
public class PriceGreaterFilterV2 implements FilterV2 {

  private final double price;

  public PriceGreaterFilterV2(double price) {
    this.price = price;
  }

  @Override
  public boolean apply(Book book) {
    return book.getPrice() > price;
  }
}
PressContainFilterV2
  • 出版社包含工业字样
public class PressContainFilterV2 implements FilterV2 {
  private final String name;

  public PressContainFilterV2(String name) {
    this.name = name;
  }

  @Override
  public boolean apply(Book book) {
    return book.getPress().contains(name);
  }
}

逻辑Filter实现

AndFilter

import java.util.List;

public class AndFilterV2 implements FilterV2 {
  private final List<FilterV2> chain;

  public AndFilterV2(List<FilterV2> chain) {
    this.chain = chain;
  }

  @Override
  public boolean apply(Book book) {
    for (FilterV2 filter : chain) {
      if (!filter.apply(book)) {
        return false;
      }
    }
    return true;
  }
}

FilterUtils

这里为了方便,使用List来替代。同时可以编写一个Util工具类,完成集合的过滤。如:

import java.util.List;
import com.google.common.collect.Lists;

public final class FilterUtils {
  private FilterUtils() {
  }

  public static List<Book> filter(List<Book> books, FilterV2 filterV2) {

    List<Book> filterBooks = Lists.newArrayList();
    for (Book elem : books) {
      if (filterV2.apply(elem)) {
        filterBooks.add(elem);
      }

    }
    return filterBooks;
  }
}

如何调用?

可以通过如下方式完成调用,获取筛选过的结果集。

FilterV2 filterV2 = new AndFilterV2( 
    Lists.newArrayList(new PriceGreaterFilterV2(50), new PressContainFilterV2("工业"))
);

List<Book> newFilterV2Books = FilterUtils.filter(filterBooks, filterV2);

采用方法二,相比较方法一,每个具体的过滤器更加简洁,过滤的时候,只要一次遍历即可。

上述给出了2种过滤器模式的实现方式,可以有效应对对象集合的筛选。
但是,其实如果使用了JDK 1.8或者更高的版本,我们可以使用Lambdas语法轻松完成。

Filter 模式(第3种实现方式: 基于 Lambdas 表达式)

  • 使用 lambdas 语法轻松搞定

使用 Predicate

定义价格大于50的Predicate
Predicate<Book> greaterThan50 = new Predicate<Book>() {
	@Override
	public boolean test(Book t) {
	    return t.getPrice() > 50;
	}
};

也可简化为:

Predicate<Book> greaterThan50 = t -> t.getPrice() > 50;
定义出版社包含工业字样的Predicate
Predicate<Book> pressContains = new Predicate<Book>() {
	@Override
	public boolean test(Book t) {
		return t.getPress().contains("工业");
	}
};

同理,这也可以简化为:

Predicate<Book> pressContains = t-> t.getPress().contains("工业");

如何调用?

使用Predicate结合stream可以轻松实现过滤

filterBooks.stream().filter(greaterThan50.and(pressContains));

延申/研究:Predicate接口组成

  • 查看Predicate源码,可以看到,其定义了一个test方法,以及andornegative方法

这些方法的思路其实和上述方式二实现的类似。

CASE : Apache common-io库,针对文件和目录的过滤

  • Apache common-io库,针对文件和目录的过滤实现,就大量使用了这种设计模式。

  • 相关文献

页内搜索关键词: Filter

CASE: Java Web 的拦截器/过滤器

  • 拦截过滤器模式Intercepting Filter Pattern)用于对应用程序的请求或响应做一些预处理/后处理
  • 定义过滤器,并在把请求传给实际目标应用程序之前应用在请求上。
  • 过滤器可以做认证/授权/记录日志,或者跟踪请求,然后把请求传给相应的处理程序。

以下是这种设计模式的实体:

  • 过滤器(Filter) - 过滤器在请求处理程序执行请求之前或之后,执行某些任务。
  • 过滤器链(Filter Chain) - 过滤器链带有多个过滤器,并在Target上按照定义的顺序执行这些过滤器。
  • Target - Target对象是请求处理程序。
  • 过滤管理器(FilterManager) - 过滤管理器管理过滤器和过滤器链。
  • 客户端(Client) - Client是向Target对象发送请求的对象。

实现思路

  • 我们将创建FilterChainFilterManagerTargetClient作为表示实体的各种对象。
  • AuthenticationFilterDebugFilter表示实体过滤器。
  • InterceptingFilterDemo类使用Client来演示拦截过滤器设计模式。

Step1 创建过滤器接口 Filter

  • Filter.java
public interface Filter {
   public void execute(String request);
}

Step2 创建实体过滤器

  • AuthenticationFilter.java
public class AuthenticationFilter implements Filter {
   public void execute(String request){
      System.out.println("Authenticating request: " + request);
   }
}
  • DebugFilter.java
public class DebugFilter implements Filter {
   public void execute(String request){
      System.out.println("request log: " + request);
   }
}

Step3 创建Target

  • Target.java
public class Target {
   public void execute(String request){
      System.out.println("Executing request: " + request);
   }
}

Step4 创建过滤器链

  • FilterChain.java
import java.util.ArrayList;
import java.util.List;
 
public class FilterChain {
   private List<Filter> filters = new ArrayList<Filter>();
   private Target target;
 
   public void addFilter(Filter filter){
      filters.add(filter);
   }
 
   public void execute(String request){
      for (Filter filter : filters) {
         filter.execute(request);
      }
      target.execute(request);
   }
 
   public void setTarget(Target target){
      this.target = target;
   }
}

Step5 创建过滤管理器

  • FilterManager.java
public class FilterManager {
   FilterChain filterChain;
 
   public FilterManager(Target target){
      filterChain = new FilterChain();
      filterChain.setTarget(target);
   }
   public void setFilter(Filter filter){
      filterChain.addFilter(filter);
   }
 
   public void filterRequest(String request){
      filterChain.execute(request);
   }
}

Step6 创建客户端Client

  • Client.java
public class Client {
   FilterManager filterManager;
 
   public void setFilterManager(FilterManager filterManager){
      this.filterManager = filterManager;
   }
 
   public void sendRequest(String request){
      filterManager.filterRequest(request);
   }
}

Step7 使用Client来演示拦截过滤器设计模式

  • InterceptingFilterDemo.java
public class InterceptingFilterDemo {
   public static void main(String[] args) {
      FilterManager filterManager = new FilterManager(new Target());
      filterManager.setFilter(new AuthenticationFilter());
      filterManager.setFilter(new DebugFilter());
 
      Client client = new Client();
      client.setFilterManager(filterManager);
      client.sendRequest("HOME");
   }
}

输出结果:

Authenticating request: HOME
request log: HOME
Executing request: HOME

小结

通过上述内容的说明,想必大家对过滤器模式已经有了一个较好的理解了。

  • 过滤模式允许开发人员使用不同的标准来过滤一组数据对象,通过逻辑运算解耦的方式把它们连接起来,这样可以快速组装过滤条件,而不需要写一大堆整合的if-else逻辑。

  • 如果使用JDK 1.8及以上版本,使用Predicate甚至不需要写任何的过滤器类即可完成。

如果不支持lambdas语法,那么上述方法二的实现思想也是一个不错的选择。

其实,common-io中,针对文件和目录的过滤实现,就是这种思想,有兴趣的读者可以自行去了解一下。

Y 推荐文献

X 参考文献

posted @ 2025-04-16 13:55  千千寰宇  阅读(118)  评论(0)    收藏  举报