How to reduce code duplication using predicates and lambda expressions

Many sites allow their clients to search for information and then browse through the results.

Each result has multiple attributes such as

  • title
  • price
  • status

 

results attributes

 

In test automation projects, it is important to have the ability of filtering the results by different attributes.

For example, filter the results with price greater than 0.

Or filter the results that are available only online.

Or filter the results that have a keyword included in the title.

 

How can this filtering be implemented?

public class Result {

  private String title;
  private int price;
  private String status;

  public Result(String title, int price, String status) {
    this.title = title;
    this.price = price;
    this.status = status;
  }

  public int price() {
    return this.price;
  }

  public String title() {
    return this.title;
  }

  public String status() {
    return this.status;
  }

}

Result class is used for creating an object for each result returned for the user search.

The results filtering happens in the test class:

 

public class TestClass {

List<Result> results;

List<Result> withPriceGreaterThan0(List<Result> list) {
  List<Result> newResults = new ArrayList<>();
  for (Result r : list)
    if (r.price() > 0)
     newResults.add(r);
  return newResults;
}

List<Result> withPriceEqualTo0(List<Result> list) {
  List<Result> newResults = new ArrayList<>();
  for (Result r : list)
    if (r.price() == 0)
     newResults.add(r);
  return newResults;
}

List<Result> byStatus(List<Result> list, String status) {
  List<Result> newResults = new ArrayList<>();
  for (Result r : list)
    if (r.status().toLowerCase().contains(status.toLowerCase()))
     newResults.add(r);
  return newResults;
}

List<Result> byKeyword(List<Result> list, String keyword) {
  List<Result> newResults = new ArrayList<>();
  for (Result r : list)
    if (r.title().toLowerCase().contains(keyword.toLowerCase()))
     newResults.add(r);
  return newResults;
}

@Before
public void setUp() {
  Result r1 = new Result("Otter Box", 54, "Not sold online");
  Result r5 = new Result("Virgin iPhone", 0, "Not sold online");
  Result r7 = new Result("Apple iPhone", 450, "Not sold online");
  Result r2 = new Result("Rogers iPhone", 130, "Sold out online");
  Result r6 = new Result("SanDisk iXpand", 125, "Sold out online");
  Result r8 = new Result("Koodo iPhone", 280, "Sold out online");
  Result r3 = new Result("Fido iPhone", 400, "Available online only");
  Result r4 = new Result("TELUS iPhone", 0, "Available online");

  results = Arrays.asList(r1, r2, r3, r4, r5, r6, r7, r8);
}

@Test
public void test1() {

  for(Result r: withPriceGreaterThan0(results))
    assertTrue(r.price() > 0);

  for(Result r: withPriceEqualTo0(results))
    assertTrue(r.price() == 0);

  for(Result r: byStatus(results, "AVAILABLE ONLINE"))
    assertTrue(r.status().toLowerCase().contains("available online"));

  for(Result r: byKeyword(results, "IPHONE"))
    assertTrue(r.title().toLowerCase().contains("iphone"));

}

}

The filtering is being done by the following methods:

List<Result> withPriceGreaterThan0(List<Result> list
List<Result> withPriceEqualTo0(List<Result> list)
List<Result> byStatus(List<Result> list, String status
List<Result> byKeyword(List<Result> list, String keyword)

All get a List<Result> parameter, browse the list with a for statement and select the results that match a specific condition.

The code of the 4 filtering methods is pretty much identical with the only difference being the condition that the results are compared against:

r.price() > 0

r.price() == 0

r.status().toLowerCase().contains(status.toLowerCase())

r.title().toLowerCase().contains(keyword.toLowerCase())

 

How can we have 1 filtering method instead of 4?

One of the new features of Java 8 is lambda expressions.

lambda expressions

Lambda expressions and predicates will help us replace the 4 methods with 1.

Lambda expressions allow functionality (code) to be used as a method parameter in addition to objects.

A lambda expression can be assigned to a Predicate object which can be used as value for a method parameter.

If you are not familiar with lambda expressions yet, this is a good introduction.

Our code can be rewritten with predicates and lambda expressions as follows:

import static org.junit.Assert.assertTrue;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.function.Predicate;

import org.junit.Before;
import org.junit.Test;

public class TestClass {

List<Result> results;

List<Result> filter(List<Result> list, Predicate<Result> p) {
  List<Result> newResults = new ArrayList<>();
  for (Result r : list)
    if (p.test(r))
      newResults.add(r);
  return newResults;
}

@Before
public void setUp() {
 ..............................
}

@Test
public void test1() {

  Predicate<Result> priceGreaterThan0 = (r) -> r.price() > 0;
  for(Result r: filter(results, priceGreaterThan0))
    assertTrue(r.price() > 0);

  for(Result r: filter(results, (r) -> r.price() == 0))
    assertTrue(r.price() == 0);

  for(Result r: filter(results, (r) -> r.status().equalsIgnoreCase("AVAILABLE ONLINE") == true))
    assertTrue(r.status().toLowerCase().contains("available online"));

  Predicate<Result> keywordInTitle = (r) ->   r.title().toUpperCase().contains("IPHONE") == true;
  for(Result r: filter(results, keywordInTitle))
    assertTrue(r.title().toLowerCase().contains("iphone"));

  for(Result r: filter(results, priceGreaterThan0.and(keywordInTitle))) {
    assertTrue(r.price() > 0);
    assertTrue(r.title().toLowerCase().contains("iphone"));
  }

}

}

In this version of the code, all conditions are expressed as lambda expressions and can be assigned or not to Predicate objects:

Predicate<Result> priceGreaterThan0 = (r) -> r.price() > 0;

Predicate<Result> keywordInTitle = (r) -> r.title().toUpperCase().contains("IPHONE") == true;

(r) -> r.price() == 0))

(r) -> r.status().equalsIgnoreCase("AVAILABLE ONLINE") == true))

Since the conditions are expressed as lambda expressions that can be assigned to a Predicate<Result> object, we can have 1 filtering method only that gets 2 parameters:

  • list of results
  • predicate

 

Can we make the code simpler?

The predicates can be moved to a separate class as follows:

import java.util.function.Predicate;

import Result;

public class Predicates {

  public static Predicate<Result> priceGreaterThan0() {
    return (r) -> r.price() > 0;
  }

  public static Predicate<Result> priceEqualTo0() {
    return (r) -> r.price() == 0;
  }

  public static Predicate<Result> availableOnline() {
    return (r) -> r.status().equalsIgnoreCase("AVAILABLE ONLINE") == true;
  }

  public static Predicate<Result> soldOutOnline() {
    return (r) -> r.status().equalsIgnoreCase("SOLD OUT ONLINE") == true;
  }

  public static Predicate<Result> titleIncludes(String keyword) {
    return (r) -> r.title().toUpperCase().contains(keyword) == true;
  }

}

The test class becomes straightforward when the predicates are in their own class:

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.function.Predicate;

import static org.junit.Assert.assertTrue;

import org.junit.Before;
import org.junit.Test;

import Result;

public class TestClass {

List<Result> results;

List<Result> filter(List<Result> list, Predicate<Result> p) {
   List<Result> newResults = new ArrayList<>();
   for (Result r : list)
     if (p.test(r)) 
       newResults.add(r);
   return newResults;
 }

@Before
public void setUp() {

...........................................

}

@Test
public void test1() {

for(Result r: filter(results, Predicates.priceGreaterThan0()))
  assertTrue(r.price() > 0);

for(Result r: filter(results, Predicates.priceEqualTo0()))
  assertTrue(r.price() == 0);

for(Result r: filter(results, Predicates.availableOnline()))
  assertTrue(r.status().toLowerCase().contains("available online"));

for(Result r: filter(results, Predicates.titleIncludes("iphone")))
  assertTrue(r.title().toLowerCase().contains("iphone"));

for(Result r: filter(results, Predicates.priceGreaterThan0()
.and(Predicates.titleIncludes("iphone")))) {
  assertTrue(r.price() > 0);
  assertTrue(r.title().toLowerCase().contains("iphone"));
}

}

}

 

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s