Behavior Parameterization with Java Lambda

learn java at geekmj

1. Intro

Behavior parameterization is a technique to change the behavior of a function on demand. You may use it to implement the strategy design pattern. Strategy design pattern [1]Strategy Design Pattern Article on Wikipedia enables flexibility and control at the caller level by allowing selection of algorithm at the time of code execution. You will see the effectiveness of this design pattern further in the tutorial.

In this guide, you will see various different ways to implement behavior parameterization in Java. It emphasizes on the concise and effective implementation using Java Lambda, which is recommended to you by us.

2. Sample Requirement – eCommerce Product filter

Evolution of a Product filtering feature of an eCommerce application after several iterations of development may look like below:

  1. User can filter on product type (Mobile). (1st iteration)
  2. User can filter on product type (Tablet). (2nd iteration)
  3. User can filter products by specifying color. (3rd iteration)
  4. User can filter products by specifying type & color. (4th iterations)

Expect more filter criteria added in the mobile listing page as application evolved. You are seeing an easy use case to make sure we understand concepts easily.

In software product development, you see the changes in requirement is bound to happen. Your challenge is to develop this feature in a way, adding new criteria not lead to verbosity, duplication and code smell.

๐Ÿ”‘ Checkout complete source code for this guide on GitHub [2]Source code for this guide. You can download or git clone the source at your local system. Import downloaded source code as Maven project in eclipse. [3]Importing Maven project in Eclipse article on Vaadin Blog. org.geekmj.java8 package consists of four main classes for execution.

3. Domain Model & Utility Classes

You need the following classes and enums:

Product Class & Enum
Product Class & Enum

3.1 Product.java

package org.geekmj.java8.model;

import lombok.AllArgsConstructor;
import lombok.Data;

@Data
@AllArgsConstructor
public class Product {
	private ProductType productType;
	private String title;
	private ProductColor productColor;
	private float screenSize;
	private float price;

	@Override
	public String toString() {
		return productType.name() + " - " + title + " - " + productColor.name() + " - " + screenSize + " - " + price;
	}
}

3.2 ProductType.java

package org.geekmj.java8.model;

public enum ProductType {
	MOBILE, TABLET
}

3.3 ProductColor.java

package org.geekmj.java8.model;

public enum ProductColor {
	WHITE, BLACK, BLUE
}

3.4 AppUtils.java

package org.geekmj.java8.util;

import java.util.ArrayList;
import java.util.List;

import org.geekmj.java8.model.Product;
import org.geekmj.java8.model.ProductColor;
import org.geekmj.java8.model.ProductType;

public abstract class AppUtils {

	public static List<Product> getSampleProducts() {
		List<Product> products = new ArrayList<>();

		products.add(new Product(ProductType.MOBILE, "Mobile 1", ProductColor.BLACK, 4, 100));
		products.add(new Product(ProductType.MOBILE, "Mobile 2", ProductColor.BLACK, 3, 50));
		products.add(new Product(ProductType.MOBILE, "Mobile 3", ProductColor.BLUE, 4, 100));
		products.add(new Product(ProductType.MOBILE, "Mobile 4", ProductColor.BLUE, 3, 50));
		products.add(new Product(ProductType.MOBILE, "Mobile 5", ProductColor.WHITE, 3, 50));
		products.add(new Product(ProductType.MOBILE, "Mobile 6", ProductColor.WHITE, 4.5f, 125));

		products.add(new Product(ProductType.TABLET, "Tablet 1", ProductColor.BLACK, 8, 100));
		products.add(new Product(ProductType.TABLET, "Tablet 2", ProductColor.BLACK, 7, 50));
		products.add(new Product(ProductType.TABLET, "Tablet 3", ProductColor.BLUE, 8, 100));
		products.add(new Product(ProductType.TABLET, "Tablet 4", ProductColor.BLUE, 7, 50));
		products.add(new Product(ProductType.TABLET, "Tablet 5", ProductColor.WHITE, 7, 50));
		products.add(new Product(ProductType.TABLET, "Tablet 6", ProductColor.WHITE, 8.5f, 125));

		return products;
	}
}

getSampleProducts() the utility static method used by main classes.

4. Basic filter implementation

In basic product filter implementation, you define a separate method for each filter criteria.

  1. filterMobileProducts() for returning product with type mobile.
  2. filterTabletProducts() for returning product with type tablet.
  3. More methods for other filtering criteria.
Illustration of the basic filter approach
Illustration of the basic filter approach

4.1 BasicFilter.java

package org.geekmj.java8.filter;

import static java.lang.System.out;

import java.util.ArrayList;
import java.util.List;

import org.geekmj.java8.model.Product;
import org.geekmj.java8.model.ProductColor;
import org.geekmj.java8.model.ProductType;

import static org.geekmj.java8.model.ProductType.*;
import static org.geekmj.java8.model.ProductColor.*;

public class BasicFilter {

	public void filter(List<Product> products) {

		out.println("** Basic Filter V1 **\n");

		out.println("\nFilter Mobile Products (Calling filterMobileProducts method)\n");

		filterMobileProducts(products).forEach(out::println);

		out.println("\nFilter Tablet Products (Calling filterTabletProducts method)\n");

		filterTabletProducts(products).forEach(out::println);

		out.println("\nFilter Blue Color Products (Calling filterBlueTabletProducts method)\n");

		filterBlueProducts(products).forEach(out::println);

		out.println("\n** Basic Filter V2 **\n");

		out.println(
				"* Basic Filter Sucks As we tend to create seperate methods just for minor BL changes, Lets make a common method and pass color and product type as parameters.*");

		out.println("\nFilter Blue Color Products (Calling filterProducts method)\n");
		filterProducts(products, null, ProductColor.BLUE, false).forEach(out::println);

		out.println("\nFilter Tablet Products (Calling filterProducts method)\n");
		filterProducts(products, ProductType.TABLET, null, true).forEach(out::println);

		out.println("\nFilter Mobile Products (Calling filterProducts method)\n");
		filterProducts(products, ProductType.MOBILE, null, true).forEach(out::println);

	}

	private List<Product> filterMobileProducts(List<Product> products) {
		List<Product> filteredProducts = new ArrayList<>();
		for (Product product : products) {
			if (MOBILE.equals(product.getProductType())) {
				filteredProducts.add(product);
			}
		}
		return filteredProducts;
	}

	private List<Product> filterTabletProducts(List<Product> products) {
		List<Product> filteredProducts = new ArrayList<>();
		for (Product product : products) {
			if (TABLET.equals(product.getProductType())) {
				filteredProducts.add(product);
			}
		}
		return filteredProducts;
	}

	private List<Product> filterBlueProducts(List<Product> products) {
		List<Product> filteredProducts = new ArrayList<>();
		for (Product product : products) {
			if (BLUE.equals(product.getProductColor())) {
				filteredProducts.add(product);
			}
		}
		return filteredProducts;
	}

	private List<Product> filterProducts(List<Product> products, ProductType type, ProductColor color, boolean isType) {
		List<Product> filteredProducts = new ArrayList<>();

		for (Product product : products) {
			if (isType && type.equals(product.getProductType()) || !isType && color.equals(product.getProductColor())) {
				filteredProducts.add(product);
			}
		}

		return filteredProducts;
	}
}

Few observations for filterMobileProducts(), filterTabletProducts() and filterBlueProducts() functions called by filter() function to return Products for different filter criteria:

  1. Their argument and return type is List<Product>.
  2. Most of the business logic of these functions are repetitive and it looks really bad. Specially iteration on List<Product>.
  3. It’s violating the Don’t Repeat Yourself principle [4]Wikipedia article on Don’t Repeat Yourself of software engineering.
  4. A change in business logic to traverse the product list lead towards changes all the method.
  5. The number of methods will keep growing as the filtering requirement will evolve.

You can enhance your code by introducing a single method which takes criteria values as parameter. You implement business using these parameter values to return the filtered list of Product. It replaces the need for multiple methods.

You can see the function filterProducts(). It takes List<Product>, ProductType, ProductColor and boolean (if true – filter on ProductType) as parameters. You call it by passing different values.

out.println("\nFilter Blue Color Products (Calling filterProducts method)\n");
filterProducts(products, null, ProductColor.BLUE, false).forEach(out::println);

out.println("\nFilter Tablet Products (Calling filterProducts method)\n");
filterProducts(products, ProductType.TABLET, null, true).forEach(out::println);

out.println("\nFilter Mobile Products (Calling filterProducts method)\n");
filterProducts(products, ProductType.MOBILE, null, true).forEach(out::println);

Few observations for filterProducts():

  1. It is better than the earlier approach where you used three different methods.
  2. You need a lot of changes within the function as filter requirement evolves.
  3. Function parameters need change as filter requirement evolves. For e.g. You want to filter on screenSize.
  4. You will end up with a large method with a lot of business logic or many methods.

You need a way to separate the business logic for iterating on Products and selection of Product meeting criteria. You will see in the next section, how Behavioral Paramameter approach helps us with that.

๐Ÿ“Note: Skipped for brevity, the code of Main Java class BasicFilterApp, which you use to run the program of this section. You can get it in the repo.[5]Source code for this guide

5. Behavior Parameterization

Consider another approach to solve the Product filtering problem by model the criteria used to select a Product. You return true when Product matches certain criteria. For e.g. if Product is of Mobile Type than return true. Similarly, if Product is of Mobile Type as well as of Blue color than true.

You will see in the following sections, how we separated the Product selection criteria business logic in separate units and used them on-demand where we are iterating over Products.

5.1 Interface & Class based

Let’s introduce the following new interfaces, classes, and methods:

  1. Define an interface FilterStrategy with one function test() which returns a boolean value. test() can also be called Predicate function (Function which returns a boolean value) [6]Wikipedia article on Predicate Function.
  2. Let’s create three different implementations for FilterStrategy interface.
    1. MobileStrategy – For filter Product of Mobile ProductType.
    2. TabletStrategy – For filter Product of Tablet ProductType.
    3. BlueColorStrategy – For filter Product of Blue Color.
    4. BlueMobileStrategy – For filter Product of Mobile Type and Blue Color.
  3. filterProducts(List<Product> products, FilterStrategy filterStrategy) – The method can take the different implementation of FilterStrategy based on need.
Illustration of FilterStrategy Interface and its implementation with function test().
Illustration of FilterStrategy Interface and its implementation with function test().

5.1.1 FilterStrategy.java

package org.geekmj.java8.strategy;

import org.geekmj.java8.model.Product;

public interface FilterStrategy {
	public boolean test(Product product);
}

5.1.2 MobileStrategy.java

package org.geekmj.java8.strategy;

import org.geekmj.java8.model.Product;
import org.geekmj.java8.model.ProductType;

public class MobileStrategy implements FilterStrategy {

	@Override
	public boolean test(Product product) {
		return ProductType.MOBILE.equals(product.getProductType());
	}
}

5.1.3 TabletStrategy.java

package org.geekmj.java8.strategy;

import org.geekmj.java8.model.Product;
import org.geekmj.java8.model.ProductType;

public class TabletStrategy implements FilterStrategy {

	@Override
	public boolean test(Product product) {
		return ProductType.TABLET.equals(product.getProductType());
	}
}

5.1.4 BlueColorStrategy.java

package org.geekmj.java8.strategy;

import org.geekmj.java8.model.Product;
import org.geekmj.java8.model.ProductColor;

public class BlueColorStrategy implements FilterStrategy {

	@Override
	public boolean test(Product product) {
		return ProductColor.BLUE.equals(product.getProductColor());
	}
}

5.1.5 BlueMobileStrategy.java

package org.geekmj.java8.strategy;

import org.geekmj.java8.model.Product;
import org.geekmj.java8.model.ProductColor;
import org.geekmj.java8.model.ProductType;

public class BlueMobileStrategy implements FilterStrategy {

	@Override
	public boolean test(Product product) {
		return ProductColor.BLUE.equals(product.getProductColor())
				&& ProductType.MOBILE.equals(product.getProductType());
	}
}

5.1.6 BehaviorParameterizationFilter.java

package org.geekmj.java8.filter;

import static java.lang.System.out;

import java.util.ArrayList;
import java.util.List;

import org.geekmj.java8.model.Product;
import org.geekmj.java8.strategy.BlueColorStrategy;
import org.geekmj.java8.strategy.BlueMobileStrategy;
import org.geekmj.java8.strategy.FilterStrategy;
import org.geekmj.java8.strategy.MobileStrategy;
import org.geekmj.java8.strategy.TabletStrategy;

public class BehaviorParameterizationFilter {

	public void filter(List<Product> products) {
		out.println("** Behavior Parameterization Filter **\n");

		out.println("\nFilter Mobile Products (Calling filterProducts with MobileStrategy)\n");
		filterProducts(products, new MobileStrategy()).forEach(out::println);
		
		out.println("\nFilter Tablet Products (Calling filterProducts with TabletStrategy)\n");
		filterProducts(products, new TabletStrategy()).forEach(out::println);
		
		out.println("\nFilter Mobile Products (Calling filterProducts with BlueColorStrategy)\n");
		filterProducts(products, new BlueColorStrategy()).forEach(out::println);
		
		out.println("\nFilter Blue Mobile Products (Calling filterProducts with BlueMobileStrategy)\n");
		filterProducts(products, new BlueMobileStrategy()).forEach(out::println);

	}

	public List<Product> filterProducts(List<Product> products, FilterStrategy filterStratgey) {
		List<Product> filteredProducts = new ArrayList<>();
		for (Product product : products) {
			if (filterStratgey.test(product)) {
				filteredProducts.add(product);
			}
		}
		return filteredProducts;
	}
}

Important observations:

  1. You are able to pass behavior (FilterStrategy implementation) to filterProducts() function at the runtime.
  2. This technique is also called the Strategy Design Pattern[7]Strategy Design Pattern Article on Wikipedia. – We define a set of algorithms and decide which algorithm will be used at runtime.
  3. Now you separated the business logic to iterate the Products and selection of Product based on specific FilterStrategy test() implementation.
  4. This technique is flexible and readable then the earlier technique for filtering.
  5. Most important code in various different FilterStrategy implementation is wrapped inside test() function.
  6. It’s better than the earlier approach, still, there are lots of verbosities, which in the long run will be a maintenance nightmare.

In the next section, you will use anonymous inner classes [8]Learn more about anonymous inner classes at oracle website to reduce this verbosity up to an extent.

๐Ÿ“Note: Skipped for brevity, the code of Main Java class BehaviorParameterizationFilterApp, which you use to run the program of this section. You can get it in the repo.[9]Source code for this guide

5.2 Anonymous inner classes based

Anonymous classes will eliminate the need for FilterStrategy implementation classes.
Anonymous classes will eliminate the need for FilterStrategy implementation classes.

You can use anonymous classes in place of actual FilterStrategy implementation class.

5.2.1 BehaviorParameterizationAnonymousFilter.java

package org.geekmj.java8.filter;

import static java.lang.System.out;

import java.util.ArrayList;
import java.util.List;

import org.geekmj.java8.model.Product;
import org.geekmj.java8.model.ProductColor;
import org.geekmj.java8.model.ProductType;
import org.geekmj.java8.strategy.FilterStrategy;

public class BehaviorParameterizationAnonymousFilter {

	public void filter(List<Product> products) {
		out.println("** Behavior Parameterization using Anonymous Class Filter **\n");

		out.println("\nFilter Mobile Products (Calling filterProducts with Anonymous Class Strategy)\n");
		filterProducts(products, new FilterStrategy() {
			@Override
			public boolean test(Product product) {
				return ProductType.MOBILE.equals(product.getProductType());
			}

		}).forEach(out::println);

		out.println("\nFilter Tablet Products (Calling filterProducts with Anonymous Class Strategy)\n");
		filterProducts(products, new FilterStrategy() {
			@Override
			public boolean test(Product product) {
				return ProductType.TABLET.equals(product.getProductType());
			}

		}).forEach(out::println);

		out.println("\nFilter Mobile Products (Calling filterProducts with Anonymous Class Strategy)\n");
		filterProducts(products, new FilterStrategy() {
			@Override
			public boolean test(Product product) {
				return ProductColor.BLUE.equals(product.getProductColor());
			}

		}).forEach(out::println);

		out.println("\nFilter Blue Mobile Products (Calling filterProducts with Anonymous Class Strategy)\n");
		filterProducts(products, new FilterStrategy() {
			@Override
			public boolean test(Product product) {
				return ProductColor.BLUE.equals(product.getProductColor())
						&& ProductType.MOBILE.equals(product.getProductType());
			}

		}).forEach(out::println);

	}

	public List<Product> filterProducts(List<Product> products, FilterStrategy filterStratgey) {
		List<Product> filteredProducts = new ArrayList<>();
		for (Product product : products) {
			if (filterStratgey.test(product)) {
				filteredProducts.add(product);
			}
		}
		return filteredProducts;
	}
}

Observations:

  1. Use of Anonymous classes further reduces the verbosity. But it has not eliminated it completely. Remember our need is to pass a single line of code which checks the Product attribute against a condition.
  2. It increased the complexity of the filter method.

In the next section, we will see how Java 8 Lambda expression has solved this problem elegantly.

๐Ÿ“Note: Skipped for brevity, the code of Main Java class BehaviorParameterizationAnonymousFilterApp, which you use to run the program of this section. You can get it in the guide git repo.[10]Source code for this guide

5.3 Lambda Expression based

You can achieve ultimate agility and elegance by using Java 8 Lambda Expression for behavioral parameterization. It eliminates verbosity and makes the code elegant.

Java 8 comes with several predefined Functional Interface (Interface with a single method) for usage, one of them is Predicate, which further eliminates the need for Interface FilterStrategy.

Cleaner approach for behavioral parameterization with Java 8 Lambda
Cleaner approach for behavioral parameterization with Java 8 Lambda

5.3.1 BehaviorParameterizationLambdaFilter.java

package org.geekmj.java8.filter;

import static java.lang.System.out;

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

import org.geekmj.java8.model.Product;
import org.geekmj.java8.model.ProductColor;
import org.geekmj.java8.model.ProductType;

public class BehaviorParameterizationLambdaFilter {
	public void filter(List<Product> products) {
		out.println("** Behavior Parameterization using Lambda Expression **\n");

		out.println("\nFilter Mobile Products (Calling filterProducts with Lambda Expression)\n");

		filterProducts(products, product -> ProductType.MOBILE.equals(product.getProductType())).forEach(out::println);

		out.println("\nFilter Tablet Products (Calling filterProducts with Lambda Expression)\n");

		filterProducts(products, product -> ProductType.TABLET.equals(product.getProductType())).forEach(out::println);

		out.println("\nFilter Mobile Products (Calling filterProducts with Lambda Expression)\n");

		filterProducts(products, product -> ProductColor.BLUE.equals(product.getProductColor())).forEach(out::println);

		out.println("\nFilter Blue Mobile Products (Calling filterProducts with Lambda Expression)\n");

		filterProducts(products, product -> ProductColor.BLUE.equals(product.getProductColor())
				&& ProductType.MOBILE.equals(product.getProductType())).forEach(out::println);
	}

	public List<Product> filterProducts(List<Product> products, Predicate<Product> predicate) {
		List<Product> filteredProducts = new ArrayList<>();
		for (Product product : products) {
			if (predicate.test(product)) {
				filteredProducts.add(product);
			}
		}
		return filteredProducts;
	}
}

Few Observations:

  1. Lambda Expression based method call filterProducts(products, product -> ProductType.MOBILE.equals(product.getProductType())) reduced the verbosity we saw while using FilterStrategy classes and anonymous classes driven approach.
  2. With Lambda Expression we declutter the unnecessary verbose and code become more readable.
  3. Java system inbuilt Function Interface java.util.function.Predicate [11]Learn more about inbuilt Predicate Functional Interfacefurther help in removing the dependency on custom made functional interface. We should leverage the inbuilt Functional Interface as much as possible.[12]Learn about Functional Interface at oracle website

๐Ÿ“Note: Skipped for brevity, the code of Main Java class BehaviorParameterizationLambdaFilterApp, which you use to run the program of this section. You can get it in the guide git repo.

6. Summary

  1. We discussed the Product filtering feature. User may want to filter product based on one attribute or combination of attributes.
  2. We implemented a filtering feature using a multi-function approach, where we have one function for every filtering need. Every function has iteration on product list logic, which leads to duplication of code.
  3. We implement filtering feature using the generic method with value parametrization approach. It reduced the need for multiple functions but increases the complexity of the individual function.
  4. We discussed another model to solve the filtering problem, where we can separate product iteration and selection of Product which satisfy certain criteria. You used the Behavior Parameterization approach to pass the logic of selection for Product meeting certain criteria passing classes implementing FilterStrategy Interface as method argument at runtime.
  5. We implemented Behavior Parameterization (Strategy Pattern)using three approaches.
    1. Class based – We implemented separate classes for specific criteria logic.
    2. Anonymous class based – We used an anonymous class for implementing FilterStrategy interface on runtime.
    3. Lambda Expression based – Finally we saw Lambda Expression based behavior parameterization.
  6. Lambda Expression based approach stands out with its elegance and agility.

References   [ + ]

Leave a Comment

Your email address will not be published. Required fields are marked *