Blog-Archiv

Montag, 22. August 2022

Refactor Java Code Using Lambdas

This Blog article is about how you can refactor repetitive Java source code by using functional interfaces (lambdas) that are predefined in the Java Development Kit. Lambdas have been improving the potential for code reusability a lot.

Repetitive Code

Recently I saw a unit-test that looked like this:

import static org.junit.Assert.*;

import java.time.LocalDate;
import java.time.ZoneId;
import java.util.Date;

import org.junit.Test;

/**
 * Tests that write a property, save to persistence, then read from
 * persistence and check if the property is in the persistent object.
 */
public class UseLambdasForRefactoringSource
{
    private Database database = new Database();
    
    @Test
    public void setPersonName()    {
        // create data
        final String name = "myTestPerson";
        final Person person = new Person();
        person.setName(name);
        
        // perform action
        database.save(person);
        final Person persistentPerson = database.load(person.getId());
        
        // assert result
        assertEquals(name, persistentPerson.getName());
    }
    
    @Test
    public void setPersonBirthday()    {
        // create data
        final Date birthday = Date.from(LocalDate.of(2015, 02, 20).atStartOfDay(ZoneId.systemDefault()).toInstant());
        final Person person = new Person();
        person.setBirthday(birthday);
        
        // perform action
        database.save(person);
        final Person persistentPerson = database.load(person.getId());
        
        // assert result
        assertEquals(birthday, persistentPerson.getBirthday());
    }

    @Test
    public void setLocationStreet()    {
        // create data
        final String street = "myTestStreet";
        final Location location = new Location();
        location.setStreet(street);
        
        // perform action
        database.save(location);
        final Location persistentLocation = database.load(location.getId());
        
        // assert result
        assertEquals(street, persistentLocation.getStreet());
    }
    
    @Test
    public void setLocationDoorNumber()    {
        // create data
        final int doorNumber = 99;
        final Location location = new Location();
        location.setDoorNumber(doorNumber);
        
        // perform action
        database.save(location);
        final Location persistentLocation = database.load(location.getId());
        
        // assert result
        assertEquals(doorNumber, persistentLocation.getDoorNumber());
    }
}

Every test method here basically does the same, the comments tell it: create test data, perform an action, then assert the result. What differs are the used entities and their properties.

This is a wide-spread way to write unit-tests, using good old copy & paste coding, although I would rather call it bad old copy & paste coding. Code duplications are bad on unit-tests like they are anywhere. Tests are so close to their test-subjects that every change in the subject can cause a change in the test, and then you have to read and understand the test, and this is not different from reading application code: it takes a lot of time! So if you avoid code duplications in your code, you should also avoid it in your tests.

Especially repetitive code is hard to read, because every line that looks like the one you have seen before could contain a subtle hard-to-see difference. The mass of repetitions finally multiplies the effort. Even experienced developers do not consider that. Or they ignore it, because they do not intend to participate in the maintenance phase of the software (which makes up 70% of production efforts).

Involved Classes

Here are the classes that take part in the tests above.

First there are the persistence entity types (as aenemic beans):

  • PersistentObject
public class PersistentObject
{
    private Long id;
    
    public Long getId() {
        return id;
    }
    public void setId(Long id) {
        this.id = id;
    }
}
  • Person
import java.util.Date;

public class Person extends PersistentObject
{
    private String name;
    private Date birthday;
    
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public Date getBirthday() {
        return birthday;
    }
    public void setBirthday(Date birthday) {
        this.birthday = birthday;
    }
}
  • Location
public class Location extends PersistentObject
{
    private String street;
    private int doorNumber;
    
    public String getStreet() {
        return street;
    }
    public void setStreet(String street) {
        this.street = street;
    }
    public int getDoorNumber() {
        return doorNumber;
    }
    public void setDoorNumber(int doorNumber) {
        this.doorNumber = doorNumber;
    }
}

Then there is a mock-implementation of a database:

import java.util.Hashtable;
import java.util.Map;

/** A mock database for tests. */
public class Database
{
    private Map<Long,PersistentObject> store = new Hashtable<>();
    private long nextId = 1L;
    
    public void save(PersistentObject entity)    {
        if (entity.getId() == null)    {
            entity.setId(nextId);
            nextId++;
        }
        store.put(entity.getId(), entity);
    }
    
    public <T extends PersistentObject> T load(Long id)    {
        if (store.containsKey(id) == false)
            throw new IllegalArgumentException("Entity not found, id = "+id);
        
        return (T) store.get(id);
    }
}

This mock-class uses a local map instead of a real database. It is just an example of some action to be taken in a test.

Ok, but how can we improve the code in UseLambdasForRefactoringSource?

There are a number of ways to do that. What I would advise against is a writing a source generator. With Java 8, I would use standard functional interfaces that were introduced with that language version.

Refactored Code

The structure of all methods is the same, just the details are different, i.e. what happens on data creation, and what happens on result assertion. So let's implement a parmeterizable abstraction of that call-sequence:

import java.time.LocalDate;
import java.time.ZoneId;
import java.util.Date;
import java.util.function.Consumer;
import java.util.function.Predicate;

import org.junit.Test;

/**
 * Tests that write a property, save to persistence, then read from
 * persistence and check if the property is in the persistent object.
 */
public class UseLambdasForRefactoringTarget
{
    private Database database = new Database();
    
    private <T extends PersistentObject> void performTest(
            T entity,
            Consumer<T> dataPreparation,
            Predicate<T> assertion,
            String assertionErrorMessage)
    {
        dataPreparation.accept(entity);
        
        database.save(entity);
        final T persistentEntity = database.load(entity.getId());
        
        if (assertion.test(persistentEntity) == false)
            throw new RuntimeException(assertionErrorMessage);
    }
}

Here we have an outline of a test:

  1. the test entity is created by the caller and given as first parameter
  2. then that entity is passed to a function that sets a test value: accept()
  3. every entity is saved to database and re-read from it afterwards
  4. finally the test value is compared with the saved value: test()

The used functional interfaces are standard classes that come bundled with the JDK:

  • Consumer is the void function that takes one parameter
  • Predicate is the function that takes one parameter and returns boolean

Other predefined useful functional interfaces are Supplier (method get) and Function (method apply). There are also variants of these interfaces with more than one parameter, e.g. BiConsumer.

Now lets use that abstraction of a test:

    @Test
    public void setPersonName()    {
        final String name = "myTestPerson";
        performTest(
                new Person(),
                (person) -> person.setName(name),
                (persistentPerson) -> name.equals(persistentPerson.getName()),
                "Person's name is not "+name);    
    }
    
    @Test
    public void setPersonBirthday()    {
        final Date birthday = Date.from(LocalDate.of(2015, 02, 20).atStartOfDay(ZoneId.systemDefault()).toInstant());
        performTest(
                new Person(),
                (person) -> person.setBirthday(birthday),
                (persistentPerson) -> birthday.getTime() == persistentPerson.getBirthday().getTime(),
                "Person's birthday is not "+birthday);    
    }

    @Test
    public void setLocationStreet()    {
        final String street = "myTestStreet";
        performTest(
                new Location(),
                (location) -> location.setStreet(street),
                (persistentLocation) -> street.equals(persistentLocation.getStreet()),
                "Location's street is not "+street);
    }
    
    @Test
    public void setLocationDoorNumber()    {
        final int doorNumber = 99;
        performTest(
                new Location(),
                (location) -> location.setDoorNumber(doorNumber),
                (persistentLocation) -> doorNumber == persistentLocation.getDoorNumber(),
                "Location's doorNumber is not "+doorNumber);
    }

Every test method calls the abstract test and parameterizes it with an entity and two lambdas. The first lambda sets the test value. The second lambda asserts that the test value actually is in the persistent entity.

Isn't this much more elegant? It is also easier to read in case you understand what a lambda is.

Conclusion

Of course the refactoring still is not perfect. We could also introduce an action-lambda, or a lambda for outputting the error message (the actual value is missing in the message).

I hope I could give some hints how you can avoid repetitive code. Java 8 lambdas help a lot. From my 25 years of coding experience I can tell you that code duplication is the worst of all developer sins. 80% of my time I have spent fixing and improving code written by persons that I did not know. If you give one problem to three developers, you will get back three different solutions. That is why software production is so expensive.