Skip to main content
Blog

1 m² of source code and a single Java interface to tame it

By 27 mei 2020No Comments

You know that most of the task planners or calendar apps have features when you create a todo, task or meeting they try to be “smart” in filling in as much details as possible?

You enter the title of a new e.g. meeting and based on some heuristics it tries to conveniently set some additional properties.

This is what Alice considers the biggest unique selling-points of her company’s new Calendar app. Development started just this morning and its kick $ss features will blow the competition out of the water!

Consider the very first version of the main Calendar application which tries to detect the start date of the meeting, based on some known keywords such as “today”:

class Calendar {

   Meeting create(String title) {
      Meeting meeting = new Meeting(title);

      // contribute possible start date
      LocalDate now = LocalDate.now();
      if (title.contains("today")) {
         meeting.startDate = now;
      } else if (title.contains("tomorrow")) {
         meeting.startDate = now.plusDays(1);
      }

      return meeting;
   }
}

The Calendar gets the text of the new entry from the user, sets the entire text into the title and because it detects a known keyword “today” in it, and sets the start date of the meeting to today’s date. We’ll worry about internationalization for another time 🙂

The following test case demonstrates this behaviour:

class CalendarTest {

   @Test
   void should_create_meeting() {

      Calendar calendar = new Calendar();

      String text = "Lunch today";
      Meeting meeting = calendar.create(text);

      assertThat(meeting.title).isEqualTo(text);
      assertThat(meeting.startDate).isToday();
   }

Changes

Now developer Bob thinks this is a very neat little Calendar system which could benefit from some additional contribution: automatically adding attendees based on a recognized name from the phone book.

interface Phonebook {
   Collection<Contact> getContacts();
}

Bob exercises some proper TDD thinking and changes the test to reflect the desired behaviour.

@Test
void should_create_meeting() {

   Phonebook phonebook = () -> Arrays.asList(
       new Contact("Alice"),
       new Contact("Bob")
   );

   Calendar calendar = new Calendar();

   String text = "Lunch today with Alice";
   Meeting meeting = calendar.create(text, phonebook);

   assertThat(meeting.title).isEqualTo(text);
   assertThat(meeting.startDate).isToday();
   assertThat(meeting.attendees)
       .extracting(Attendee::getName)
       .containsOnly("Alice");
}

To make the test pass, he updates the Calendar to find contacts in the phone book (mentioned by name) and automatically add them as an attendee to the meeting.

class Calendar {

   Meeting create(String title, Phonebook phonebook) {
      Meeting meeting = new Meeting(title);

      // contribute possible start date
      LocalDate now = LocalDate.now();
      if (title.contains("today")) {
         meeting.startDate = now;
      } else if (title.contains("tomorrow")) {
         meeting.startDate = now.plusDays(1);
      }

      // contribute possible attendees
      meeting.attendees.addAll(
          phonebook.getContacts()
              .stream()
              .filter(contact -> title.contains(contact.name))
              .map(Attendee::new)
              .collect(Collectors.toList())
      );

      return meeting;
   }
}

Changes

Now co-worker Chris picks up a new user story which reads:

Given a possible location in the title / When you can detect a correct address / Set the meeting’s location to the found address

He knows a pretty decent online API for (fuzzy) searching for addresses and makes the necessary changes to the main body of the Calendar class.

As soon as he commits his work to version control, he encounters some merge conflicts: seems Alice has updated the Calendar already in the meantime with some changes to the start-date-detection-algorithm.

Chris gets Alice’s new updates, merges them with his own code locally and re-submits again to version control. Seems Bob beat him to it! There was a bug in the attendee-finding-logic, but Bob squashed it already – that’s how he rolls. Anyway, whether Chris could back away a little from that general Calendar area at all the rest of the day, if he didn’t mind? Senior-architect Dave just came back from holiday and fancies some development work himself. Not too impactful, some variable renaming or something.

Changes

That does it!, co-worker Eddy thinks when he sees his colleague Chris crying. He says: “Why don’t you isolate these changes from each other in the application? Every part which contributes something to a meeting can work in isolation, developed in isolation and tested in isolation. There’s no need for everything to happen inside the Calendar itself, because as you’ve noticed: integrating your changes with other people’s changes continuously on the same 1 m² of source code is a friggin’ goat rodeo!”.

Then Eddy did it:

interface Contributor {
   void contribute(Meeting meeting);
}

He created an interface.

Chris was baffled! And thinking: “The beauty of it! The Calendar itself doesn’t care about the start-date-contribution-algorithm, the attendee-finding-logic and probably also not about Chris’ own work-in-progress of detecting a location.”

“No, it doesn’t” says Chris, as if he could have overheard Chris thinking. “Each and every individual behaviour can all be abstracted away into their own hidden implementations of that interface. All the Calendar knows there are parts of the software which it needs to collaborate with. Everybody can contribute something, it just needs to give everyone a chance.”

That’s some deep stuff!

It’s good to have a test in place, so together they reckon it’s pretty safe to apply some refactorings.

Changes

Alice moves her start-date-contribution-algorithm to a separate class:

class StartDateContributor implements Contributor {

   @Override
   public void contribute(Meeting meeting) {
      LocalDate now = LocalDate.now();
      String title = meeting.title;
      if (title.contains("today")) {
         meeting.startDate = now;
      } else if (title.contains("tomorrow")) {
         meeting.startDate = now.plusDays(1);
      }
   }
}

Bob follows and moves his attendee-finding-logic to a separate class too:

class AttendeeContributor implements Contributor {

   final Phonebook phonebook;

   AttendeeContributor(Phonebook phonebook) {
      this.phonebook = phonebook;
   }

   @Override
   public void contribute(Meeting meeting) {

      meeting.attendees.addAll(
          phonebook.getContacts()
              .stream()
              .filter(contact -> meeting.title.contains(contact.name))
              .map(Attendee::new)
              .collect(Collectors.toList())
      );
   }
}

Finally, Chris can actually also find a spot for his work-in-progress code of trying to detect a location, and creates a separate class too:

class LocationContributor implements Contributor {

   final GoogleMaps maps;

   LocationContributor(GoogleMaps maps) {
      this.maps = maps;
   }

   @Override
   public void contribute(Meeting meeting) {
      maps.findAddress(meeting.title)
          .ifPresent(address -> meeting.location = address);
   }
}

Chris knows his feature is far from perfect, but that’s OK. He also knows he can make improvements without having to touch anybody else’s code!

Without all the features removed, the Calendar is in a sorry state, according to the tests.

Luckily Eddy quickly manages to make the necessary adjustments and really trims the main Calendar logic down to just calling the contributors:

class Calendar {

   final List<Contributor> contributors;

   Calendar(Contributor... contributors) {
      this.contributors = Arrays.asList(contributors);
   }

   Meeting create(String title) {
      Meeting meeting = new Meeting(title);
      for (Contributor contributor : contributors) {
         contributor.contribute(meeting);
      }
      return meeting;
   }

}

at the same time he makes the test actually use the contributors:

@Test
void should_create_meeting() {

   Phonebook phonebook = () -> Arrays.asList(
       new Contact("Alice"),
       new Contact("Bob")
   );

   Calendar calendar = new Calendar(
       new StartDateContributor(),
       new AttendeeContributor(phonebook)
   );

   String text = "Lunch today with Alice";
   Meeting meeting = calendar.create(text);

   assertThat(meeting.title).isEqualTo(text);
   assertThat(meeting.startDate).isToday();
   assertThat(meeting.attendees)
       .extracting(Attendee::getName)
       .containsOnly("Alice");
}

Test changes

Chris, eager to bring also his location contributor also under test, adds it to test in the initialization too…

Calendar calendar = new Calendar(
    new StartDateContributor(),
    new AttendeeContributor(phonebook),
    new LocationContributor(new GoogleMaps())
);

...

…before Eddy steps in: “Not on my watch, Chris!”

Chris: “What do you mean?”

Eddy: “Look at the Calendar, man. It hardly does anything at all, except call one or more contributors. That’s the only thing actually worth checking in the Calendar test. You think it cares for your location feature, Chris?”. To Alice and Bob (which quietly had Chris take the heat when they knew very well were this was heading) “Well, let me tell you, it’ll no longer carry your feature’s water!”

It took a while for the team to realize that for their own isolated code they also should have their own tests now. Not only would the team conflict again on another patch of 1 m² of test source code, but having the Calendar tests fail based on the correctness of their ever evolving contributors would definitely come up at the company BBQ. Shivers!

Alice moves creates a start-date-contribution-algorithm test in a separate class:

class StartDateContributorTest {

   @Test
   void should_start_meeting_today() {

      StartDateContributor contributor = new StartDateContributor();

      Meeting meeting = new Meeting("Update report today");
      contributor.contribute(meeting);

      assertThat(meeting.startDate).isToday();
   }
}

Bob follows and creates an attendee-finding-logic test in a separate class too:

class AttendeeContributorTest {

   @Test
   void should_add_existing_contact_as_attendee() {

      Phonebook phonebook = () -> Arrays.asList(
          new Contact("Alice"),
          new Contact("Bob")
      );

      AttendeeContributor contributor = new AttendeeContributor(phonebook);

      Meeting meeting = new Meeting("Play with Bob");
      contributor.contribute(meeting);

      assertThat(meeting.attendees)
          .extracting(Attendee::getName)
          .containsOnly("Bob");
   }
}

Eddy changes the original test to verify if the Calendar actually calls whatever contributor is passed in.

@Test
void should_create_meeting() {

   Contributor contributor = mock(Contributor.class);

   Calendar calendar = new Calendar(contributor);

   String text = "Lunch today with Alice";
   Meeting meeting = calendar.create(text);

   assertThat(meeting.title).isEqualTo(text);
   verify(contributor).contribute(meeting);
}

Chris thinks such a test which asserts two things should actually be two separate tests, and he’s read somewhere it’s better to use test doubles instead of mocks, but he reckons that’s not the kind of decisions Eddy wanted to showcase here. What a great guy!

In the weeks after

Alice has made here start-date-contribution-algorithm i18n-aware and support 34 languages, including Klingon. She never touched the Calendar class, or test for that matter, ever again.

Bob started making the Calendar itself more versatile by introducing concepts which all could benefit from the same contributing logic, such as Appointments, Tasks, Todo’s, Diaries, Bullet Journals by introducing a general Thing abstraction.

Chris built the entire contribution mechanism out to a full-fledged plugin-mechanism leveraged by an online Marketplace boasting more than 500+ plugins which all perform a better job a location-detection than Chris’ own version ever did.

This has been cross-post of my personal blog.