🤩How to write extensible filter logic in Java code
In this article, I will detail how to use the Predicate<T> interface for maintaining multiple filters in the application code.
Hi! My name is Narendra Vardi, and I write about my learnings, observations and trends in the Software world. Besides software, I also talk about photography, travel stories, books and movies. If that's something that interests you, consider subscribing.
This is one of those articles in which I really wanted to give an idea of how I think. Hopefully, it doesn't only make sense to you but you would love the way this article is written! :)
There is a lot of code in this article. But it is simple code. Don't feel intimidated! :D
The below problem is similar to a problem that I encountered in the industry. In this article, the problem is simplified! Now let's dive in.
Imagine that you are given a collection of Property
objects (class code provided below) and you need to return all the properties whose location is blr
.
How will you approach it?
If you are new to Java, you might write a for loop and do a check on location. The sample function might look like the below code.
The only problem with this code is if we want to get properties with location hyd
, we will have to write an additional function.
Instead of this, we can update the existing function to accept the location that needs to be searched for as a function argument. Let's see how the code evolves.
public static List<Property> getFilteredProperties(List<Property> properties, String locationToSearch) { // notice locationToSearch is added here
List<Property> result = new ArrayList<>();
for (Property p: properties) {
if (p.getLocation().equals(locationToSearch)) {
result.add(p);
}
}
return result;
}
Now, we want to add a filter on price also. I mean providing a price range filter is awesome for customers isn't it? We learnt already that passing search filter values as function arguments makes code generic enough to accept different values. So, we will accept the lower and higher property thresholds from the function argument itself.
Now how does the code evolve?
public static List<Property> getFilteredProperties(List<Property> properties, String locationToSearch,
double lowerPrice, double higherPrice) { // lowerPrice and higherPrice added here
List<Property> result = new ArrayList<>();
for (Property p: properties) {
if (p.getLocation().equals(locationToSearch)
&& p.getPrice() <= higherPrice && p.getPrice() >= lowerPrice) { // logic to check if the price is within price range is added here.
result.add(p);
}
}
return result;
}
Now the customer wants to filter on twobhk
propertyType
.
Let's see how the code looks.
public static List<Property> getFilteredProperties(List<Property> properties, String locationToSearch,
double lowerPrice, double higherPrice,
String propertyType) { propertyType added here
List<Property> result = new ArrayList<>();
for (Property p: properties) {
if (p.getLocation().equals(locationToSearch)
&& p.getPrice() <= higherPrice && p.getPrice() >= lowerPrice
&& propertyType.equals(p.getPropertyType())) { // propertyType check added here
result.add(p);
}
}
return result;
}
Problems with this function include:
- Increase in the number of arguments for the function as we add
more
filters. - The
if
condition keeps getting complicated and the code is getting changed in the code execution path which can directly impact the production flow in case of any bugs.
How do we solve this problem?
The first problem can be fixed if we use a new class to hold the function arguments.
Code after the first fix:
public static List<Property> getFilteredProperties(List<Property> properties, FilterParams filterParams) { // look how filterParams helps here.
List<Property> result = new ArrayList<>();
for (Property p: properties) {
if (p.getLocation().equals(filterParams.getLocation())
&& p.getPrice() <= filterParams.getUpperPrice() && p.getPrice() >= filterParams.getLowerPrice()
&& filterParams.getPropertyType().equals(p.getPropertyType())) {
result.add(p);
}
}
return result;
}
Now let's solve the following:
The if condition keeps getting complicated and the code is getting changed in the code execution path which can directly impact the production flow in case of any bugs
To fix this problem, we will use Predicate functional interface (please read about this interface as I am not explaining it in detail in this article). I will show how to use the Predicate interface by modifying the location-only filter example.
How to filter on location using Predicate
interface?
// getFilteredProperties without using Predicate interface.
public static List<Property> getFilteredProperties(List<Property> properties, String locationToFilter) {
List<Property> result = new ArrayList<>();
for (Property p: properties) {
if (p.getLocation().equals(locationToFilter)) {
result.add(p);
}
}
return result;
}
// The above can be transformed using Predicate in this format.
// First create an implementation of Predicate.
// Here we are creating one class for LocationFilter.
public class LocationFilter implements Predicate<Property> {
private String location;
public LocationFilter(String location) {
this.location = location;
}
@Override
public boolean test(Property property) {
return property.getLocation().equals(location);
}
}
// The function looks like the following:
public static List<Property> getFilteredProperties(List<Property> properties, String locationToFilter) {
List<Property> result = new ArrayList<>();
for (Property p: properties) {
Predicate<Property> locFilter = new LocationFilter(locationToFilter);
if (locFilter.test(p)) {
result.add(p);
}
}
return result;
}
If you notice the above code, locFilter has taken up the responsibility of abstracting the location filter logic.
Similarly, we can add a new Filter for price and PropertyType.
We have three implementations of Predicate
interface and we can use this interface to extend our filter functionality easily in future.
We have PropertyTypeFilter.java
, LocationFilter.java
and PriceFilter.java
files.
To segregate these three filters, let's use an additional helper
method as listed below.
Now the function code becomes, the following:
public static List<Property> getFilteredProperties(List<Property> properties, FilterParams filterParams) {
return properties
.stream()
.filter(FilterHelper.getFilters(filterParams))
.collect(Collectors.toList());
}
If I have seen this code during my college days or right after college, I would have thought, why complicate the code by moving the logic into multiple files? But today, I realise the value of extensibility. The above code is incredible in case of code extensibility and since the Filter logic is independent, we can change the FilterLogic however we want without disturbing the actual function.
I have created a Github repo that hosts the final code detailed in this article. Find the Github repo at github.com/narendravardi/MultiFilters
How did you feel the code is? Do you have any queries? Please feel free to comment below or shoot an email.
Share this article
Copy and share this article: narendravardi.com/java-predicate/
Recommendations
If you liked this article, you might also enjoy reading the following.
- Future Software Engineers, read this article before and during your placements
- 🤖Automating the boring stuff: How I obtained the titles of all articles on my blog
- ✍️Finding the Nth highest salary from an SQL table
❤️ Enjoyed this article?
Forward to a friend and tell them where they can subscribe (hint: it's here).
Anything else? Comment below to say hello, or drop an email!