Improving Code Clarity with Functional Programming in Java.

jmnovelovargas

Miguel Novelo

Posted on September 28, 2023

Improving Code Clarity with Functional Programming in Java.

Today, after a month of working on a new feature, we were ready to have a bug bash, which essentially saved me before ramping this new feature to the public.

As I began debugging my code, I found myself examining a piece of code that utilizes streams, a crucial tool for functional programming in Java when working with collections.

I appreciate how elegant, concise, and readable the code becomes when using map to transform items, apply filters, find minimum and maximum values. However, this time, my lack of experience using these features worked against me.

So, What happened?

I was working with a piece of code performing some transformations like this:

entities.stream()
  .map(Entity::getId)
  .map(new EntityCriteria()::setEntity)
  .collect(Collectors.toList())
Enter fullscreen mode Exit fullscreen mode

If you're more experienced than I was when I wrote this code, you might have already spotted the issue. Allow me to explain:

  1. entities is a collection (List) and its elements are type of Entity
  2. map(Entity::getId) will transform the elements into their IDs
  3. The intention of map(new EntityCriteria()::setEntity) was to transform the entity IDs into EntityCriteria objects with the entityId. However, this happened, but not in the right way.

Understanding the Concepts.

To comprehend what is wrong with number 3, we need to grasp some functional programming concepts:

  • Entity::getId from the Entity class uses the :: operator to retrieve a Function called getId. It does NOT retrieve the actual ID.
    This Function will be applied to the Entity elements to retrieve their IDs because Entity is a class, and getId doesn't accept any arguments.

  • When we call map(new EntityCriteria()::setEntity), new EntityCriteria() creates a new object. Then ::setEntity on that object generates a Function capable of applying setEntity to the new EntityCriteria object.

Root Cause

The Function created by new EntityCriteria()::setEntity is always applied to the same EntityCriteria object, It doesn't create new objects and then retrieve the function to apply setEntity.

The Most Important Question: Why this wasn't caught in Unit Tests ?

The mocked data used in the unit tests, replicated the same buggy logic for transforming the entities, so everything appeared to work.

Possible solutions:

Using Lambdas

map(entityId -> new EntityCriteria().setEntity(entityId)) 
Enter fullscreen mode Exit fullscreen mode

When we use lambdas, we define the function where the entityId on the left is the input to the function, and the right part is the body of the function (In this case, the transformation logic).

Creating a new method to be applied in map

private static EntityCriteria createEntityCriteria(long entityId) {
  return new EntityCriteria().setEntity(entityId);
}

...

entities.stream()
  .map(Entity::getId)
  .map(MyClass::createEntityCriteria)
  .collect(Collectors.toList())

Enter fullscreen mode Exit fullscreen mode

This approach creates a Function that applies createEntityCriteria and that function generates a new EntityCriteria object for each element.

Conclusion

  1. Everybody makes mistakes, so make sure to thoroughly test your code.
  2. When writing mock data in unit tests, avoid replicating the logic for building the transformation if that is what you are actually testing.
💖 💪 🙅 🚩
jmnovelovargas
Miguel Novelo

Posted on September 28, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related