/ general

Behaviour Parameterization with lambda expressions

This write up is about passing behaviour as parameter using lambdas in Java.

Little Background:

Java’s lambda expressions are just anonymous functions which you can define on the go. These anonymous functions are the bread and butter for functional programming in java.

Let’s assume we have a class for Apple

public enum Color {
   red,
   green;
}
import lombok.AllArgsConstructor;
import lombok.Getter;


@AllArgsConstructor
@Getter
public class Apple {
    private Integer weight;
    private Color color;
    private Integer price;
}

Let’s also assume that there’s a farmer who cultivates apples and you are supposed to create an application for him where based on the attribute of choice we should let the farmer filter apples.

One fine day the farmer turns up and informs you that he needs filtering on Color, so you come up with a filter method who’s definition is like this

public List<Apple> filterByColor(final Color color, final List<Apple> apples){
    final List<Apple> filteredApples = new ArrayList<>();
    for(Apple apple: apples){
        if(apples.getColor().equals(color)){
            filteredApples.add(apple);
        }
    }
    return filteredApples;
}

But what you forgot to perceive is the golden rule of software, requirements keep changing :D, so another fine day the farmer turns up and tells you that the application needs filter by weight support as well, so after this you go into thinking mode and think of all the scenarios for filter given the Apple class has three attributes weight, color, price so your first shot at solving this problem is something like

private void filterByColor(final Color color, final List<Apple> filteredApples){
        for(Apple apple: apples){
            if(apples.getColor().equals(color)){
                filteredApples.add(apple);
            }
        }
}

private void filterByWeight(final Integer weight, final List<Apple> filteredApples){
        for(Apple apple: apples){
            if(apples.getWeight() >= weight){
                filteredApples.add(apple);
            }
        }
}

private void filterByPrice(final Integer price, final List<Apple> filteredApples){
        for(Apple apple: apples){
            if(apples.getPrice() >= price){
                filteredApples.add(apple);
            }
        }
}

public List<Apple> filter(final Color color, final Integer weight, final Integer price, final List<Apple> apples){
    final List<Apple> filteredApples = new ArrayList<>();
    for(int i=0; i<apples.size(); i++){
        if(color != null){
            filterByColor(color, filteredApples);
        }
        if(weight != null){
            filterByWeight(weight, filteredApples);
        }
        if(price != null){
           filterByPrice(price, filteredApples);
        }

    }
    return filteredApples;    
}

so the requirement is satisfied and the farmer is happy, but there is a serious issue with this kind of implementation, notice it ?

  1. All the attributes are passed as parameters to the filter method, so let’s say you want to filter by only color, you will end up passing color to the filter method and the rest two parameters being null.
  2. Let’s say that in the future you add another attribute in the Apple class which is also supports filter behaviour then the filter method needs to change and the clumsy if block needs to be added.
  3. Code duplication, observe that most of the code is same except for the if clause in the filterBy methods.

Can we do better? Definitely yes, Java 8 lambda expressions to the rescue I assume you are aware of Functional Interfaces in java, if not i recommend you to have a in depth read about it. One such functional interface is called Predicate, whose definition is as follows

interface Predicate<T>{
    boolean test(T t);
       
    default Predicate<T> and(Predicate<? super T> other) {
        Objects.requireNonNull(other);
        return (t) -> test(t) && other.test(t);
    }
    
    default Predicate<T> negate() {
            return (t) -> !test(t);
    }
    
    default Predicate<T> or(Predicate<? super T> other) {
        Objects.requireNonNull(other);
        return (t) -> test(t) || other.test(t);
    }
    
    static <T> Predicate<T> isEqual(Object targetRef) {
        return (null == targetRef)
                ? Objects::isNull
                : object -> targetRef.equals(object);
    }
}

our method of interest here is the test method which takes an object of Type T, so let’s see how this can be used to filter our Collection of apples efficiently

Let’s define three implementations of the Predicate class with parameter T being Apple

public RedApplePredicate implements Predicate<Apple> {
    @Override
    boolean test(Apple apple){
        return red.equals(apple.getColor());
    }
}

public HeayApplePredicate implements Predicate<Apple> {
    @override
    boolean test(Apple apple){
        return apple.getWeight() >= 150;
    }
}

now you may be thinking that since the parameters to filter are hard coded in the above implementation even this seems daunting because you will have to create classes for each category of filter, we can avoid this as well through an in line implementation for the Predicate class like this

final Predicate<Apple> redApplePredicate = (a) -> a.getColor().equals(red);
final Predicate<Apple> greenApplePredicate = (a) -> a.getColor().equals(green);

clearly the above code seems very elegant now let’s apply this to our filter method

public List<Apple> filter(final List<Apple> apples, final Predicate<Apple> filterBehaviour){
    final List<Apple> filteredApples = new ArrayList();
    for(Apple apple: apples){
        if(filterBehaviour.test(apple)){
            filteredApples.add(apple);
        }
    }
    return filteredApples;
}

so now our filter method accepts a predicate, say now we have to filter for green apples with weight > 150, we could easily do this by

public static void main(String[] args){
   final Predicate<Apple> greenApplePredicate = (a) -> a.getColor().equals(green);
   final Predicate<Apple> heavyApplePredicate = (a) -> a.getWeight() >= 150;
   final Apple redApple = new Apple(150, red, 150);
   final Apple greenApple = new Apple(100, green, 150);
   final Apple greenHeavyApple = new Apple(150, green, 150);
   final List<Apple> apples = Arrays.asList(redApple, greenApple, greenHeavyApple);
   final List<Apple> filteredApples = this.filter(apples, greenApplePredicate.and(heavyApplePredicate));
}

You can clearly see that the implementation above is very elegant and easily modifyable without requiring major code changes. That’s the power of behaviour parameterization.

Happy Coding!!!

Kumar D

Kumar D

Software Developer. Tech Enthusiast. Loves coding 💻 and music 🎼.

Read More
Behaviour Parameterization with lambda expressions
Share this