How to use Lamda expressions? – Part 2 – Applying Lamda Expressions

Let’s apply lamda expressions to a specific use case and see how it helps in building better code.

Consider this use case:

A manager wants to give promotion to an employee.

He follows the below process for this :

  1. Check if the employee is eligible
  2. Evaluate the employee through interview
  3. Grant promotion based on the above outcomes.

Let’s dive into coding this without using lamdas:

Approach 1: Tightly coupled code

To represent the Employee , let’s create an employee class:

package lamda;

public class Employee {

	String name;

	private int experienceInCompany;

	private String skillSet;

	private int totalExperience;

	int rating;

	public Employee(String name, int experienceInCompany, String skillSet, int totalExperience, int rating) {

		this.name = name;
		this.setExperienceInCompany(experienceInCompany);
		this.setSkillSet(skillSet);
		this.setTotalExperience(totalExperience);
		this.rating = rating;
	}

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}

	public int getRating() {
		return rating;
	}

	public void setRating(int rating) {
		this.rating = rating;
	}

	public String getSkillSet() {
		return skillSet;
	}

	public void setSkillSet(String skillSet) {
		this.skillSet = skillSet;
	}

	public int getExperienceInCompany() {
		return experienceInCompany;
	}

	public void setExperienceInCompany(int experienceInCompany) {
		this.experienceInCompany = experienceInCompany;
	}

	public int getTotalExperience() {
		return totalExperience;
	}

	public void setTotalExperience(int totalExperience) {
		this.totalExperience = totalExperience;
	}

}

A simple POJO class with the attributes name , skillSet, experienceInCompany , totalExperience and rating of the employee.

Let’s design the manager class:

package lamda;

public class Manager {

	String name;

	public void processPromotion(Employee employee) {

		System.out.println("Checking eligibility criteria for employee: " + employee.getName());

		if (employee.getSkillSet().contains("Java") && employee.getExperienceInCompany() > 3
				&& employee.getTotalExperience() > 10) {

			int marks = interviewEmployee(employee);

			if (marks > 80) {

				grantPromotion(employee);
			}

		}

	}

	private void grantPromotion(Employee employee) {

		System.out.println("Granting promotion to employee :" + employee.getName());

	}

	private int interviewEmployee(Employee employee) {

		// do interview

		System.out.println("Interviewing  employee :" + employee.getName());
		return 85;
	}

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}

}

Let’s look at the method processPromotion(Employee).

Here is where all the action takes place.

The manager gets a reference to the employee instance and checks few conditions to see if the employee is eligible for promotion:

  • Skillset contains Java
  • Experience in current company is greater than 3 years
  • Total experience is greater than 3 years

Once the manager is done with checking the employee’s eligibility the manager is evaluating the employee through an interview. And the manager is providing marks based on the performance. If it is greater than 80 percent , the candidate is considered for promotion.

Once the evaluation is complete , the manager grants the employee promotion probably taking help of a HR.

To run this method from client code , we may do this :

        	

                Employee employee = new Employee("Kumar", 4, "Java, Spring, AWS", 11, 4);

		Manager manager = new Manager();
		manager.setName("Rahul");
		manager.processPromotion(employee);

And the promotion is processed.

There are few issues in the design.

What if the eligibility criteria changes tomorrow ? You need to change the manager domain class for that.

Why don’t we loosely couple this functionality by delegating it to another class whose sole responsibility is managing the eligibility criteria?

And then look at the second step,

The manager evaluates the employee through an interview . What if tomorrow he decides he won’t take a personal interview but may ask some one to do it on behalf of him or may be skip the interview and evaluate based on some other criteria or just wants the pass percentage to be 90 percent?

You need to change the manager domain class again for that.

Why don’t we loosely couple this functionality by delegating it to another class.

And look at the last step.

Based on the outcome of eligibility criteria and evaluation he grants promotion. A manager very likely will need help of HR in processing promotion.

Why don’t we delegate this to another class?

Considering these changes , let’s redesign our application.

Let’s create interfaces for each of the three functionality (checking eligibility criteria, evaluation and granting promotion).

We will create implementation for these interfaces.

If there is any change in the implementation tomorrow we can create new implementation classes or just change the specific class alone.

Approach 2: Loosely coupled code.

Here is the updated Manager class:

package lamda.optimized;

import lamda.Employee;

public class ManagerOptimized {

	String name;

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}

	public void processPromotion(Employee employee, CheckPromotionCriteria promotionCriteria,
			EmployeeEvaluator evaluator, HRManager hrManager) {

		if (promotionCriteria.checkPromotionEligibility(employee)) {

			int marks = evaluator.evaluateEmployee(employee);

			if (marks > 80) {

				hrManager.grantPromotion(employee);
			}

		}

	}

}

As you see each step in the promotion process is handed over to a different class.

The eligibility checking responsibility is passed to a CheckPromotionCriteria class.

The evaluation responsibility is passed to an EmployeeEvaluator class.

The promotion granting responsibility is passed to a HRManager class.

Here are their respective interfaces:

CheckPromotionCriteria.java:

package lamda.optimized;

import lamda.Employee;

public interface CheckPromotionCriteria {

	public boolean checkPromotionEligibility(Employee employee);

}

EmployeeEvaluator.java :

package lamda.optimized;

import lamda.Employee;

public interface EmployeeEvaluator {

	int evaluateEmployee(Employee employee);

}

HRManager.java:

package lamda.optimized;

import lamda.Employee;

public interface HRManager {

	void grantPromotion(Employee employee);

}

Here are the implementations of the above interfaces . If there is any change in the implementation , we need to change only those classes . And that is ok , since that is their sole purpose . As an alternative we can create new implementations which obey the interface definitions .

CheckPromotionCriteriaImpl.java

package lamda.optimized;

import lamda.Employee;

public class CheckPromotionCriteriaImpl implements CheckPromotionCriteria {

	@Override
	public boolean checkPromotionEligibility(Employee employee) {

		System.out.println("Checking eligibility criteria for employee: " + employee.getName());

		if (employee.getSkillSet().contains("Java") && employee.getExperienceInCompany() > 3
				&& employee.getTotalExperience() > 10) {

			return true;

		}

		return false;
	}

}

Just the eligibility checking logic in Manager class has been moved here.

EmployeeEvaluatorImpl.java

package lamda.optimized;

import lamda.Employee;

public class EmployeeEvaluatorImpl implements EmployeeEvaluator {

	@Override
	public int evaluateEmployee(Employee employee) {

		int marks = interviewEmployee(employee);

		return marks;
	}

	private int interviewEmployee(Employee employee) {

		System.out.println("Interviewing employee :" + employee.getName());
		return 82;
	}

}

Just the evaluation logic is separated from Manager class and included here. Tomorrow if the Manager decides not to interview and follow some other evaluation process it can be changed here easily .

HRManagerImpl.java

package lamda.optimized;

import lamda.Employee;

public class HRManagerImpl implements HRManager {

	@Override
	public void grantPromotion(Employee employee) {

		System.out.println("Granting promotion to employee: " + employee.getName());

	}

}

The promotion granting responsibility is passed to a HRManager.

Now let’s look at the client code :

                

                ManagerOptimized managerOptimized = new ManagerOptimized();
		managerOptimized.setName("The Better Rahul");

		CheckPromotionCriteria promotionCriteria = new CheckPromotionCriteriaImpl();
		EmployeeEvaluator evaluator = new EmployeeEvaluatorImpl();
		HRManager hrManager = new HRManagerImpl();

		managerOptimized.processPromotion(employee, promotionCriteria, evaluator, hrManager);



As you see any implementation can be plugged in and passed to processPromotion() method dynamically.

If you had noticed one thing , all the interfaces used above just have one method. And an interface with a single method is a functional interface.

And Java has its own predefined functional interfaces which we can reuse instead of creating our own.

That is our next optimization:

Approach 3: Use Functional interfaces provided by Java.

Let’s replace our functional interfaces:

  • CheckPromotionCriteria interface can be replaced by Predicate<T> interface.

Both do the same thing , return a boolean response . The Predicate interface has a test() method which we can use to implement the same logic what we implement inside checkPromotionEligibility() method of CheckPromotionCriteria interface.

  • EmployeeEvaluator interface can be replaced by Function<T,R> interface.

Function<T,R> interface takes an input T and returns a response R with T and R , generic arguments.

We do the same in evaluateEmployee() method , we take an employee instance as input and return a integer output.

  • HRManager interface can be replaced by Consumer<T> interface.

Both have a single method which takes a single argument as input and returns no response.

In this approach we remove all our custom interfaces and change the implementation classes. Here are the updated implementation classes:

CheckPromotionCriteriaImpl.java:

package lamda.optimized.functional.interfaces;

import java.util.function.Predicate;

import lamda.Employee;

public class CheckPromotionCriteriaImpl implements Predicate<Employee> {

	@Override
	public boolean test(Employee employee) {

		System.out.println("Checking eligibility criteria for employee: " + employee.getName());
		if (employee.getSkillSet().contains("Java") && employee.getExperienceInCompany() > 3
				&& employee.getTotalExperience() > 10) {

			return true;

		}

		return false;
	}

}

As you see the interface Predicate<Employee> is now implemented instead of CheckPromotionCriteria interface.

The method test() is used instead of checkPromotionEligibility().

Here are the other classes:

EmployeeEvaluatorImpl.java

package lamda.optimized.functional.interfaces;

import java.util.function.Function;

import lamda.Employee;

public class EmployeeEvaluatorImpl implements Function<Employee, Integer> {

	@Override
	public Integer apply(Employee employee) {

		int marks = interviewEmployee(employee);

		return marks;
	}

	private int interviewEmployee(Employee employee) {

		System.out.println("Interviewing employee: " + employee.getName());
		return 83;
	}

}

The method evaluateEmployee() is replaced by apply() method of Function<Employee,Integer> interface.

HRManagerImpl.java

package lamda.optimized.functional.interfaces;

import java.util.function.Consumer;

import lamda.Employee;

public class HRManagerImpl implements Consumer<Employee> {

	@Override
	public void accept(Employee employee) {

		System.out.println("Granting promotion to employee: " + employee.getName());

	}

}

The method grantPromotion() is replaced by accept() method which belongs to the Consumer<Employee> functional interface.

Here is the updated Manager classs:

package lamda.optimized.functional.interfaces;

import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;

import lamda.Employee;

public class ManagerOptimizedWithFI {

	String name;

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}

	public void processPromotion(Employee employee, Predicate<Employee> promotionCriteria,
			Function<Employee, Integer> evaluator, Consumer<Employee> hrManager) {

		if (promotionCriteria.test(employee)) {

			int marks = evaluator.apply(employee);

			if (marks > 80) {

				hrManager.accept(employee);
			}

		}

	}

}

As you see the interface arguments have been replaced by the functional interfaces provided by Java.

Now let’s invoke this method from client code :

		
		ManagerOptimizedWithFI managerFI = new ManagerOptimizedWithFI();
		managerFI.setName("The Even Better Rahul");

		Predicate<Employee> promotionCriteriaPredicate = new lamda.optimized.functional.interfaces.CheckPromotionCriteriaImpl();

		Function<Employee, Integer> evaluatorFunction = new lamda.optimized.functional.interfaces.EmployeeEvaluatorImpl();

		Consumer<Employee> hrManagerConsumer = new lamda.optimized.functional.interfaces.HRManagerImpl();

		managerFI.processPromotion(employee, promotionCriteriaPredicate, evaluatorFunction, hrManagerConsumer);

We are passing functional interface implementations to processPromotion() method.

The above approach is good but not good enough.

We have provided our own implementation classes .

Remember lamda can be used to pass implementations as arguments for functional interfaces.

That is the next approach we are taking.

Approach 4: Use lamda expressions

With lamda , you can get rid of all implementation classes and custom interfaces,

Instead you can create lamda expressions and pass it to processPromotion() method.

Our code now contains only two classes Employee and Manager apart from the client code.

And there is no change needed to the Manager class from the previous approach.

Only the client code changes now and here is it:

                   



                Predicate<Employee> promotionCriteriaLamda = e -> e.getSkillSet().contains("Java")
				&& e.getExperienceInCompany() > 3 && e.getTotalExperience() > 10;

		

                Function<Employee, Integer> evaluatorLamda = e -> {
			System.out.println("Interviewing employee: " + employee.getName());
			return 82;
		};

		Consumer<Employee> hrManagerLamda = e -> System.out
				.println("Granting promotion to employee: " + employee.getName());

		managerFI.processPromotion(employee, promotionCriteriaLamda, evaluatorLamda, hrManagerLamda);

As you see we are creating three lamda expressions.

One implementing the Predicate interface:

 
e -> e.getSkillSet().contains("Java")
				&& e.getExperienceInCompany() > 3 && e.getTotalExperience() > 10;

Here e denotes an employee instance ,

the expression after the arrow is the implementation part.We are passing this expression as one of the arguments to the method processPromotion()

We have another lamda implementing the Function interface:

 e -> {
			System.out.println("Interviewing employee: " + employee.getName());
			return 82;
		};

This implementation is also passed to processPromotion() method

And we have one more lamda expression implementing the Consumer interface:

 
e -> System.out
				.println("Granting promotion to employee: " + employee.getName());

That’s it.

A further optimization to the above client code is to pass the lamda expressions directly like this:

managerFI.processPromotion(employee,
				e -> e.getSkillSet().contains("Java") && e.getExperienceInCompany() > 3 && e.getTotalExperience() > 10,
				e -> {
					System.out.println("Interviewing employee: " + employee.getName());
					return 82;
				}, e -> System.out.println("Granting promotion to employee: " + employee.getName()));

This reduces the client code to a single line but looks a bit inelegant.

And there you go !

We have implemented lamda expressions for a use case.

By doing so ,

  • We can now change any of our three implementations (checking eligibility / evaluation / granting promotion) dynamically
  • Our code is a lot shorter , we just have two domain classes (Employee and Manager ) apart from the client code
  • Our code is loosely coupled , we have moved away the promotion steps to lamda expressions and freed the manager class of those responsibilities.

Lamdas are cool , isn’t it?

Here is the full code with the different approaches in different packages:

https://github.com/vijaysrj/lamdaexpressions


Posted

in

,

by

Comments

Leave a Reply

Discover more from The Full Stack Developer

Subscribe now to keep reading and get access to the full archive.

Continue reading