The Filter pattern allows us to filter a collection of objects using various conditions(filters) and chaining them in a decoupled way through logical operations. This design pattern is useful when we want to select subset of objects that satisfies one or multiple conditions. It comes under structural pattern.
In this tutorial, we will explore the Filter Design Pattern in Java, discussing its structure, implementation, best practices, and advantages.
Structure of the Filter Design Pattern
- Filter Interface : An interface responsible for declaring method(s) to filter objects. It establishes a shared contract for implementation by all concrete filters.
- Concrete Filters : Classes that implement the filter interface. Each concrete filter delineates a particular filtering criterion and embeds the logic for applying this criterion to objects.
- Criteria Interface : An interface outlining criteria objects, encapsulating individual filtering criteria. It establishes a common contract for all concrete criteria classes.
- Concrete Criteria : Classes implementing the criteria interface. Each concrete criteria class signifies a specific filtering condition and contains the logic to evaluate whether an object meets the specified criteria.
- Filter Manager : An optional component managing and amalgamating various filters and criteria. It offers a centralized mechanism for applying multiple filters to a set of objects, potentially coordinating the application of diverse filters and criteria.
- Object to be Filtered : Denoting the object or collection requiring filtration based on specified criteria. This serves as the target for the filtering process, where filters and criteria are applied to achieve the desired outcome.
Advantages of Filter Pattern
- Modularity and Reusability : The primary advantage of the Filter pattern is its ability to provide modularity and reusability in filtering logic. Criteria and filter components can be reused across different objects and scenarios.
- Separation of Concerns : By maintaining the filtering mechanism apart from the objects being filtered, the pattern helps to separate the concerns. This division facilitates the extension or modification of filtering criteria and improves maintainability.
- Flexibility in Criteria Composition : The pattern enables complicated filtering conditions to be created with flexible criteria composition. There are many ways to combine criteria, including using composite filters or logical operations (AND, OR).
- Dynamic Criteria Addition : The system's adaptability is improved by the ability to dynamically add or delete criteria at runtime. This is especially helpful in situations where the needs for filtering could change regularly.
- Easy Testing : It is simple to test different criteria and filters separately due to the modular and contained design of the filter pattern. Filtering logic can be made robust and dependable by doing independent unit tests on each criterion and filter.
- Scalability : The filter pattern scales well as the number of objects or filtering criteria rises. The system can handle a variety of filtering scenarios and new criteria or filters can be introduced without affecting already-written code.
- Consistent Filtering Logic : The pattern makes sure that filtering rules are applied consistently by enclosing filtering logic inside criteria and filter classes. Sustaining the integrity of the filtering process requires this constancy.
For Example: Let, EmployeeList be the list of all employees of a company and we want to select all Male employees whose salary is between 1000 to 5000. In this case we need three filters.
Male Filter : Returns List of all Male employees.
LessThanFilter : Returns List of employees whose salary is less than 5000.
GreaterThanFilter : Returns List of employees whose salary is greater than 1000.
Now, we have to perform logical And of above mentioned three filters. Let's see how filter pattern can help us in solving this problem.
Implementation of Filter Design Pattern
Employee.javapublic class Employee { private String name; private String gender; private String department; private int salary; public Employee(String name, String gender, String department, int salary){ this.name = name; this.gender = gender; this.department = department; this.salary = salary; } public String getName() { return name; } public String getGender() { return gender; } public String getDepartment() { return department; } public int getSalary(){ return salary; } }
We will define Filter interface having checkCondition method which takes a List of employees as input and then returns a subset of employees who satisfy the filter criteria. Every filter criteria class must implement this interface.
Filter.javaimport java.util.List; public interface Filter { public List<Employee> checkCondition(List<Employee> employees); }
GenderFilter.java
import java.util.List; import java.util.ArrayList; public class GenderFilter implements Filter { private String gender; public GenderFilter(String gender){ this.gender = gender; } @Override public List<Employee> checkCondition(List<Employee> employees){ List<Employee> filterredEmployees = new ArrayList<Employee>(); for(Employee e : employees){ if(e.getGender().equalsIgnoreCase(gender)){ filterredEmployees.add(e); } } return filterredEmployees; } }
DepartmentFilter.java
import java.util.List; import java.util.ArrayList; public class DepartmentFilter implements Filter { private String department; public DepartmentFilter(String department){ this.department = department; } @Override public List<Employee> checkCondition(List<Employee> employees){ List<Employee> filterredEmployees = new ArrayList<Employee>(); for(Employee e : employees){ if(e.getDepartment().equalsIgnoreCase(department)){ filterredEmployees.add(e); } } return filterredEmployees; } }
SalaryGreaterThanFilter.java
import java.util.List; import java.util.ArrayList; public class SalaryGreaterThanFilter implements Filter { private int value ; public SalaryGreaterThanFilter(int value){ this.value = value; } @Override public List<Employee> checkCondition(List<Employee> employees){ List<Employee> filterredEmployees = new ArrayList<Employee>(); for(Employee e : employees){ if(e.getSalary() > value){ filterredEmployees.add(e); } } return filterredEmployees; } }
SalaryLessThanFilter.java
import java.util.List; import java.util.ArrayList; public class SalaryLessThanFilter implements Filter { private int value ; public SalaryLessThanFilter(int value){ this.value = value; } @Override public List<Employee> checkCondition(List<Employee> employees){ List<Employee> filterredEmployees = new ArrayList<Employee>(); for(Employee e : employees){ if(e.getSalary() < value){ filterredEmployees.add(e); } } return filterredEmployees; } }
AndFilter.java takes a List of Filters and then perform the logical and of all filters. The output of one filter becomes the input of other in chained manner. It is used for implementing composite criteria like, "List of employees who are Male and works in CSE department".
AndFilter.javaimport java.util.List; public class AndFilter implements Filter { private List<Filter> filterList; public AndFilter(List<Filter> filterList){ this.filterList = filterList; } @Override public List<Employee> checkCondition(List<Employee> employees){ List<Employee> filterredEmployees = employees; for(Filter filter : filterList){ filterredEmployees = filter.checkCondition(filterredEmployees); } return filterredEmployees; } }
FilterPatternExample.java first creates a list of employees and the filter them using specific filter classes to gets subset of employees satisfying particular condition(s).
import java.util.List; import java.util.ArrayList; public class FilterPatternExample { public static void main(String[] args) { List<Filter> filterList = new ArrayList<Filter>(); List<Employee> employeeList= new ArrayList<Employee>(); employeeList.add(new Employee("Jack", "Male", "CSE", 1000)); employeeList.add(new Employee("Rose", "Female", "ECE", 2000)); employeeList.add(new Employee("George", "Male", "CSE", 3000)); employeeList.add(new Employee("Mike", "Male", "ECE", 4000)); employeeList.add(new Employee("Julia", "Female", "CSE", 5000)); // Define various filters Filter departmentFilter = new DepartmentFilter("CSE"); Filter maleFilter = new GenderFilter("Male"); Filter lessThanFlter = new SalaryLessThanFilter(5000); Filter greaterThanFilter = new SalaryGreaterThanFilter(1000); System.out.println("List of Male Employees"); printEmployeesName(maleFilter.checkCondition(employeeList)); System.out.println("List of CSE Employees"); printEmployeesName(departmentFilter.checkCondition(employeeList)); System.out.println("List of Male Employees whose salary is" + " greater than 1000 and less than 5000"); filterList.add(lessThanFlter); filterList.add(greaterThanFilter); filterList.add(maleFilter); Filter andFilter = new AndFilter(filterList); printEmployeesName(andFilter.checkCondition(employeeList)); } private static void printEmployeesName(List<Employee> empList){ for(Employee e : empList){ System.out.print(e.getName() + ", "); } System.out.println("\n"); } }
Output
List of Male Employees Jack, George, Mike, List of CSE Employees Jack, George, Julia, List of Male Employees whose salary is greater than 1000 and less than 5000 George, Mike,
Best Practices of Filter Pattern
- Define clear and concise criteria interfaces that capture the essential aspects of filtering. Avoid unnecessary complexity in the criteria interfaces to enhance ease of use.
- Keep the filtering logic separate from the objects being filtered. This separation of concerns promotes modularity and makes it easier to add or modify criteria without affecting the object classes.
- Provide a mechanism to combine different criteria to create complex filtering conditions. This can be achieved through composite filters or by allowing the composition of criteria through logical operations (AND, OR).
- Design criteria components to be reusable across different objects or scenarios. Create generic criteria that can be applied to various types of objects.
- Consider using a filter manager or a similar component to centralize the management and application of multiple filters. This can simplify the client code and provide a unified way to handle filtering.
- Design the filter pattern to allow dynamic addition or removal of criteria at runtime. This flexibility enables the system to adapt to changing requirements without major modifications.
- Define filter methods in a generic way to accept criteria without a specific reference to concrete criteria classes. This allows for easier extension and addition of new criteria.
- Avoid hardcoding specific criteria within objects or filters. Instead, provide a mechanism for clients to define and apply custom criteria as needed.
Strategy Design Pattern |
Builder Design Pattern |
Abstract Factory Design Pattern |
Flyweight Design Pattern |
Facade Design Pattern |
List of Design Patterns |