Eric Weise

Finance and Technology Blog

Pushing Side Effects to the Side

Functional programming provides a treasure trove of useful patterns, one of which is pushing side effects to the edges of your application. Side effects are functions that have non-deterministic behavior. Examples include making an Http call or persisting data to the database These calls can fail due to network issues or other outside influences. But side effects can also be functions that don’t fail such as retrieving the current time or a random number, since they don’t always give the same value back. By isolating these types of calls, we can more easily test our code.

The following is an example loosely based on a refactoring I did at work. The use case is similar to Google Calendar where events re-occur over a period of time or for a number of iterations. An example code snippet from our implementation looked similar to the following;

class EventService {

  public List<RecurringEvent> generateEvents(RecurringEvent recurringEvent) {
    List<Event> generatedEvents = new ArrayList<>();
    LocalDate nextDate = recurringEvent.startDate;
    int generatedCount = 0;
    LocalDate maximumDate = Local.now().plusDays(180);

     while(generatedCount < recurringEvent.maxNumber
           && !nextDate.isAfter(recurringEvent.endDate))
           && !nextDate.isAfter maximumDate) {

        Event newEvent = createEvent(recurringEvent, nextDate);
        entityManager.persist(newEvent);
        generatedEvents.add(newEvent);
        generatedCount++;
        nextDate = Recurrence.nextDate(nextDate, recurringEvents.repeatPattern);
    }
    return generatedEvents;
  }
}

This code has some non-trivial conditions that determines how many events to generate. Testing it would involve checking each of the conditions. At a minimum however, we can test that it generates some events.


class EventServiceTest extends SpringyTest {

  @Autowired
  private EntityManager EntityManager;

  private EventService eventService = new EventService(entityManager);

  @test
  public void testgenerateEvents() {
    RecurringEvent recurringEvent = RecurringEventFixture.create();
    List<Event> generatedEvents = eventService.generateEvents(recurringEvents);

    // assert the correct number of events were generated
    Assert.assertEquals(4, generateEvents.size());
  }

}

While the test looks fairly straightforward, it is actually complex because it needs to connect to a database in order to succeed. We need to ensure that the database starts and ends in a clean state and that no other tests interfere with the data while the test is running. Otherwise, we may get a different result or db related failure. The test also incurs a large performance penalty since it needs to wire up a connection pool and any other dependencies in the Spring configuration.

In the refactored code, the EntityManager.persist call is removed from the business logic. Notice that the LocalDate.now call is also removed. Remember that we want to always get the same result given the same method argument. Having the current date directly in the business logic could cause the function to behave differently depending on what day it is. Have you ever had tests that worked fine most days but failed at the beginning or end of the month? Moving the dates outside of the business logic should help.

class EventService {

  public void generateAndPersistRecurringEvents(RecurringEvent recurringEvent) {
      List<RecurringEvent> newEvents = generateEvents(recurringEvent, LocalDate.now());
      newEvents.forEach(EntityManager::persist);
  }

  public List<RecurringEvent> generateEvents(RecurringEvent recurringEvent, LocalDate currentDate) {
    List<Event> generatedEvents = new ArrayList<>();
    LocalDate nextDate = recurringEvent.startDate;
    int generatedCount = 0;
    LocalDate maximumDate = currentDate.plusDays(180);

     while(generatedCount < recurringEvent.maxNumber
           && !nextDate.isAfter(recurringEvent.endDate))
           && !nextDate.isAfter maximumDate) {

        Event newEvent = createEvent(recurringEvent, nextDate);
        generatedEvents.add(newEvent);
        generatedCount++;
        nextDate = Recurrence.nextDate(nextDate, recurringEvents.repeatPattern);
    }
    return generatedEvents;
  }
}

Our test now becomes a simple unit test instead of a Spring/database type integration test.

class EventServiceTest  {

  @test
  public void testGenerateEvents() {
    RecurringEvent recurringEvent = RecurringEventFixture.create();
    LocalDate today = LocalDate.of(2017, 1, 1);
    List<RecurringEvent> generatedEvents = EventService.generateEvents(recurringEvent, today);

    // assert the correct number of events were generated
    Assert.assertEquals(4, generateEvents.size());
  }

}

How many methods could you turn into simple pure function calls simply by removing side effect calls such as repositories from your business logic? Most projects I have worked on just sprinkle database calls throughout service layer.

In general the pattern I try to follow is;

  1. Retrieve all the data necessary to perform the business logic (side effect code).
  2. Perform the business logic using in-memory data (pure functions).
  3. Persist changes to the database (side effect code).

These three steps can’t be followed every time but when they can, the code will be much easier to test.

12 Jun 2017