🤩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.

🤩How to write extensible filter logic in Java code
Photo by Nathan Dumlao / Unsplash

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.

// package and import details 

public class Property {
    private String location;
    private double price;
    private String id;
    private String PropertyType;
    private String ownerId;
    private boolean isSoldOut;

    // boilerplate code 
}
<p><span>Property.java file with no boilerplate code</span></p>

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.

public static List<Property> getFilteredProperties(List<Property> properties) {
        List<Property> result = new ArrayList<>();
        for (Property p: properties) {
            if (p.getLocation().equals("blr")) {
                result.add(p);
            }
        }
        return result;
    }
<p><span>Iterate and search for 'blr' location properties.</span></p>

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:

  1. Increase in the number of arguments for the function as we add more filters.
  2. 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 class FilterParams {
    private String location;
    private double lowerPrice;
    private double upperPrice;
    private String propertyType;
    // boilerplate code.     
}
<p><span>FilterParams.java</span></p>
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.


public class PriceFilter implements Predicate<Property> {
    private double lowerPrice;
    private double upperPrice;

    public PriceFilter(double lowerPrice, double upperPrice) {
        this.lowerPrice = lowerPrice;
        this.upperPrice = upperPrice;
    }

    @Override
    public boolean test(Property property) {
        return property.getPrice() >= lowerPrice && property.getPrice() <= upperPrice;
    }
}
<p><span>PriceFilter.java</span></p>

public class PropertyTypeFilter implements Predicate<Property> {
    private String propertyType;

    public PropertyTypeFilter(String propertyType) {
        this.propertyType = propertyType;
    }

    @Override
    public boolean test(Property property) {
        return propertyType.equals(property.getPropertyType());
    }
}
<p><span>PropertyTypeFilter.java</span></p>

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.


public class FilterHelper {
    public static Predicate<Property> getFilters(FilterParams filterParams) {
        Predicate<Property> locationFilter = new LocationFilter(filterParams.getLocation());
        Predicate<Property> priceFilter = new PriceFilter(filterParams.getLowerPrice(), filterParams.getUpperPrice());
        Predicate<Property> propertyPredicate = new PropertyTypeFilter(filterParams.getPropertyType());

        // If a new filter needs to be added, you can add it here.
        // To avoid bugs, that new filter's `test` method can use an 'AB testing strategy' to roll out the filter to 100% safely.
        Predicate<Property> compositeFilter = locationFilter.and(priceFilter).and(propertyPredicate); // Using an AND operation here for OR operation, use `.or()` method
        return compositeFilter;
    }
}
<p><span>FilterHelper.java file</span></p>

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.


❤️ 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!