Skip to content

Instantly share code, notes, and snippets.

@jlacar
Last active April 19, 2020 23:03
Show Gist options
  • Save jlacar/fb7daddfa4ce54d3d52f78cb5e12c83d to your computer and use it in GitHub Desktop.
Save jlacar/fb7daddfa4ce54d3d52f78cb5e12c83d to your computer and use it in GitHub Desktop.
Filtering in Java using predicates, lambdas, and enums implementing the Strategy pattern

Filtering in Java using Predicate, lambdas + closures, and enums as strategies

This example shows how you can create a flexible way to filter a set of data. The program allows you to specify different conditions and ways to apply them to the data. The design uses Predicates, lambdas, and enums to implement the Strategy Pattern.

Description

The program has a list of Person objects that can be filtered in different ways. One way is to filter by age range. Another is by partial name. Any number of filters can be defined. The filters can be applied to the data in two ways: 1) accept items that match any of the criteria and 2) accept items that match all of the criteria.

Examples

Suppose the program starts with the following list of Person objects

name=Bob Burgess, age=34
name=Cathy Burgess, age=29
name=Michael Johns, age=15
name=Kim Johnwell, age=27
name=Mary Stevens, age=37
name=Kimberly Johnson, age=21

Then an age range filter is defined:

Enter age range (low high): 12 22
Filter added: Person.age in [12..22]

Since there is only filter defined, both the ANY and ALL strategies filter the list the same way:

name=Michael Johns, age=15
name=Kimberly Johnson, age=21

However, if another filter is added:

Enter name (or part of it): Kim
Filter added: Person.name contains "Kim"

then the list where ANY of the filters match would be

name=Michael Johns, age=15
name=Kim Johnwell, age=27
name=Kimberly Johnson, age=21

whereas the list where ALL of the filters match would be

name=Kimberly Johnson, age=21

Implementation Notes

The following sections explain the key parts of the design.

Person value object

The data class on which the filtering will be applied. For simplicity, the name and age attributes are made accessible.

MatchStrategy enum

The values of this enum define the different strategies for composing the predicates that will be used to filter the data set. Each enum value provides its own implementation of the abstract accumulator() method. The return type is a BinaryOperator<Predicate<Person>> that is used in the Stream.reduce() operation invoked in the Main.matches() method.

The ANY enum value uses Predicate.or() to compose all the filters defined by the user.

The ALL enum value uses Predicate.and() to compose all the filters defined by the user.

Main.showList() method

Prints out all the matching Person objects that meet the criteria applied per the given MatchStrategy.

Main.matches() method

Returns a composed Predicate<Person> based on the given MatchStrategy. It uses the strategy's accumulator() to combine all the user-defined filters into a single composed predicate. For example, ANY provides an accumulator that will compose/chain all the user-defined filters using the Predicate.or() method.

The linchpin to all this is the .reduce(strategy.accumulator()) call. This step in the stream processing produces an Optional<Predicate<Person>>, hence the call to .orElse() at the end to return a concrete Predicate<Person>. The argument to .orElse() is noFilter -> true which tries to convey the net effect of having no filter since the predicate always returns true. The name was intentionally chosen to read like a noFilter flag is being set to true.

PersonFilter class

A Static Factory class that provides different methods for producing a Predicate that captures user-defined parameters. This gives the user the flexibility to define different "customized" filters based on specific values they provide. To add more types of criteria, just add another factory method that encapsulates the predicate logic in a similar fashion as the other factory methods.

PersonFilter.byAgeRange() method

Returns a Predicate<Person> to filter People based on their ages falling within a certain range. The user enters the low and high boundaries of the desired range. When applied, the filter will accept Person objects with age that falls in the given range (inclusive of both boundaries).

The returned closed lambda captures the low and high values entered by the user.

PersonFilter.nameContains() method

Returns a Predicate<Person> to filter People based on their names. The user enters a name or part of a name. When applied, the filter will accept Person objects in which the entered string appears anywhere in Person.name.

The returned closed lambda captures the partialName value entered by the user.

Additional References

1 - What is the difference between a "closure" and a "lambda": You keep saying "closure" but I don't think it means what you probably think it means. See the answer by SasQ at https://stackoverflow.com/questions/220658/what-is-the-difference-between-a-closure-and-a-lambda

import java.util.*;
import java.util.function.*;
/**
* Sample program to show use of Predicates and composing Predicates,
* capturing variables in lambdas expressions, and using enums to
* implement the Strategy pattern.
*
* Inspired by this discussion on CodeRanch.com:
* https://coderanch.com/t/729390/engineering/Design-pattern-good-command-pattern
*/
class Main {
private static Scanner input = new Scanner(System.in);
private List<Predicate<Person>> filters = new ArrayList<>();
private List<String> filterDescriptions = new ArrayList<>();
private List<Person> people = new ArrayList<>();
{
people.addAll(Arrays.asList(
new Person("Bob Burgess", 34),
new Person("Cathy Burgess", 29),
new Person("Michael Johns", 15),
new Person("Liz Carter", 12),
new Person("Bob Jameson", 47),
new Person("Jim Stevens", 24),
new Person("Edgar Burris", 58),
new Person("Rudolfo Kimmick", 57),
new Person("Carter Johns", 37),
new Person("Jason Mark Carter", 72),
new Person("Burton Stevenson", 33),
new Person("Tom Church", 58),
new Person("Vladimir Oliva", 52),
new Person("Cathy Burton", 42),
new Person("Kim Johnwell", 27),
new Person("Mary Stevens", 37),
new Person("Kimberly Johnson", 21)
));
}
public static void main(String[] args) {
new Main().runDemo();
}
void runDemo() {
int action = 0;
do {
action = getMenuChoice();
switch (action) {
case 1: { addAgeFilter(); break; }
case 2: { addNameFilter(); break; }
case 3: { showList(MatchStrategy.ANY); break; }
case 4: { showList(MatchStrategy.ALL); break; }
case 5: { showFilters(); break; }
case 6: { clearFilters(); break; }
case 7: { showFullList(); break; }
case 8: { addPeople(); break; }
}
} while (action != 0);
}
int getMenuChoice() {
System.out.print("\nMenu:\n" +
"1 - Add age range filter\n" +
"2 - Add name filter\n" +
"3 - Show matches ANY criteria\n" +
"4 - Show matches ALL criteria\n" +
"5 - Show all filters\n" +
"6 - Clear filters\n" +
"7 - Show all people\n" +
"8 - Add people\n" +
"0 - Exit\n" +
"Choice: ");
int choice = input.nextInt();
input.nextLine();
return choice;
}
void showList(MatchStrategy strategy) {
if (filters.isEmpty()) {
showFullList();
} else {
System.out.printf("People who meet %s of the criteria:%n", strategy);
people.stream()
.filter(matches(strategy))
.forEach(System.out::println);
}
}
void showFullList() {
System.out.println("All the people in the list:");
people.stream().forEach(System.out::println);
}
void showFilters() {
if (filterDescriptions.isEmpty()) {
System.out.println("No filters defined.");
} else {
System.out.println("Filters defined:");
filterDescriptions.stream().forEach(System.out::println);
}
}
void addAgeFilter() {
System.out.print("Enter age range (low high): ");
int low = input.nextInt();
int high = input.nextInt();
input.nextLine();
filters.add(PersonFilter.byAgeRange(low, high));
addFilterDescription(String.format("Person.age in [%d..%d]", low, high));
}
void addNameFilter() {
System.out.print("Enter name (or part of it): ");
String partialName = input.nextLine().trim();
filters.add(PersonFilter.nameContains(partialName));
addFilterDescription(String.format("Person.name contains \"%s\"", partialName));
}
Predicate<Person> matches(MatchStrategy strategy) {
return filters.stream()
.reduce(strategy.accumulator())
.orElse(noFilter -> true);
}
void addFilterDescription(String description) {
System.out.printf("Filter added: %s%n", description);
filterDescriptions.add(description);
}
void clearFilters() {
filters.clear();
filterDescriptions.clear();
System.out.println("All filters cleared.");
}
void addPeople() {
System.out.println("Enter 'DONE' to go back:");
do {
System.out.print("<name>#<age> : ");
String entry = input.nextLine();
if (entry.startsWith("DONE")) break;
String[] parts = entry.trim().split("#");
addPerson(parts[0], Integer.valueOf(parts[1]));
} while (true);
showFullList();
}
void addPerson(String name, int age) {
people.add(new Person(name, age));
System.out.printf("Added person: %s%n", people.get(people.size()-1));
}
}
enum MatchStrategy {
ANY {
@Override
BinaryOperator<Predicate<Person>> accumulator() {
return (composed, criteria) -> composed.or(criteria);
}
},
ALL {
@Override
BinaryOperator<Predicate<Person>> accumulator() {
return (composed, criteria) -> composed.and(criteria);
}
};
abstract BinaryOperator<Predicate<Person>> accumulator();
};
class Person {
String name;
int age;
Person(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return String.format("name=%s, age=%d", name, age);
}
}
/* Static Factory for Predicate<Person> */
class PersonFilter {
private PersonFilter() {}
/* NOTE: Captured parameters should be declared final */
static Predicate<Person> byAgeRange(final int low, final int high) {
// lambda is closed by capturing low and high (its "closure")
return person -> low <= person.age && person.age <= high;
}
static Predicate<Person> nameContains(final String partialName) {
// lambda is closed by capturing partialName (its "closure")
return person -> person.name.contains(partialName);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment