Improving Code Clarity with Functional Programming in Java.
Tags: java, lambda, learning
Published: 2023-09-28 01:13:13

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())
```

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)) 
```

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())

```
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.