implementation details everywhere

A Tale of Implementation and Detail

As a testing advocate, I frequently delve into “implementation details” and “public interfaces.” These terms refer to the inner workings of your API and how it’s presented to users. Rather than just defining them, let’s explore their significance through a real-world example. Witness how grasping these concepts enhances our code in practical scenarios.

Once Upon a Date

Our story begins with a date. Not just any date, though – a date picker.

The Date Picker Component

While the date picker seems straightforward – show an input and a dialog with a calendar – a lot is going on behind the scenes.

For instance, in order to generate the calendar, one needs to generate the grid correctly. Notice that while we have five weeks in the grid, the days of week are not always the same day of the month. In addition, the “padding” of the first and last weeks with days of last and next month is also not constant.

The first step would be to generate the data set we will render according to. This will be the part we will focus on in this article.

Let’s see how it was implemented.

Testing and Creating the Grid

We want to create the utility function buildCalendarGrid. As its name suggests, it’s going to build the grid for us. The implementation is not that important, but for reference, I’m going to share an image of the tests so you can see they are quite comprehensive. The code is folded, so there is no need to read:

The calendar tests

Let’s take a quick look at the implementation that makes the tests pass. Again, there’s no need to read the code – we’ll dive into relevant parts of it later on. I add it here just so you could get an impression:

export const buildCalendarGrid = (
{ month, year }: Month,
locale: DatePickerLocale
): CalendarGrid => {
// Shift week days to start from firstDayOfWeek
const firstDayOfWeek = locale.firstDayOfWeek;
const getShiftedDay = (date: Date): number =>
(date.getDay() – firstDayOfWeek + 7) % 7;
const grid: CalendarGridDate[][] = [];
const firstDay = new Date(year, month, 1);
const lastDay = new Date(year, month + 1, 0);
const daysInMonth = lastDay.getDate();
const firstDayInWeek = getShiftedDay(firstDay);
let week: CalendarGridDate[] = [];
// Fill in the days before the first day of the month
for (let i = 0; i < firstDayInWeek; i++) {
const date = addDays(firstDay, i – firstDayInWeek);
week.push({
date: formatDateStr(date),
label: `${date.getDate()}`,
isOutsideMonth: true,
});
}
// Fill up days of the month
for (let i = 1; i <= daysInMonth; i++) {
week.push({
date: formatDateStr(new Date(year, month, i)),
label: `${i}`,
isOutsideMonth: false,
});
if (week.length === 7) {
grid.push(week);
week = [];
}
}
// Fill in the days after the last day of the month
const daysInLastWeek = week.length;
for (let i = daysInLastWeek; i < 7; i++) {
const date = addDays(lastDay, i – daysInLastWeek + 1);
week.push({
date: formatDateStr(date),
label: `${date.getDate()}`,
isOutsideMonth: true,
});
}
if (week.length > 0) {
grid.push(week);
}
return {
weekdays: getWeekdays(locale),
grid,
};
};
buildCalendarGrid implementation

That’s a pretty big function, right? Just looking at it causes some cognitive load. The thought of diving into such a long function might be discouraging. Anyway, this implementation makes the tests pass, so it works. Let’s summarize what’s happening there:

  1. It starts with setting local variables
  2. A loop to set the pre-month padding days.
  3. A loop to set the month days.
  4. A loop to add the post-month padding days.
  5. An if statement to see if we actually have these days and push them to the array.
  6. Return the grid and the weekdays in the calendar grid object.

Lucky us the weekdays are returned from a different function, otherwise we would have gotten ourselves with a much bigger function.

So… this function works. We can merge it, right?

Technically, yes.

What? Were you waiting for a “but”?

Ok, if you insist.

The Detail is in the Implementation

We should separate the implementation from the interface. The interface includes the function’s parameters, name, and returned value. The implementation of the algorithm is the details. The implementation detail.

If we change the implementation without changing the interface, it would mean we are doing a refactor.

I’m going to share with you another implementation. This implementation uses just one loop without the external if. Is it clearer? Easier to read? Less intimidating? More considerate for future generations of developers? You’ll be the judge of that.

const grid = [];
let week: CalendarGridDate[] = [];
const lastDay = new Date(year, month + 1, 0);
const firstDayInWeek = getDay(firstDay);
const daysInMonth = lastDay.getDate();
const daysOutsideMonthInLastWeek = 7 – getDay(lastDay);
const totalDaysInCalendar = daysInMonth + firstDayInWeek + daysOutsideMonthInLastWeek;
for (let i = 0; i < totalDaysInCalendar; i++) {
const dayIndexInMonth = i – firstDayInWeek;
week.push(createGridDate(addDays(firstDay, dayIndexInMonth), isOutsideMonth(dayIndexInMonth, daysInMonth)));
if (week.length === 7) {
grid.push(week);
week = [];
}
}
One loop to rule them all!

But wait! Can this small snippet replace three loops and an if statement?

How to Make Sure a Refactor Didn’t Break My Code?

We made a pretty big change to the code. How can we be certain it still works the same?

Remember that we started by looking at the tests? This is where they shine. The tests ensure that if we change the interface while refactoring, we’ll get notified. Changing the interface is called “A Breaking Change”. Unintentionally changing the interface is called “A Bug in Production”. If we have tests in place, the warning is hard to miss:

A message we get when something breaks

Tell you a secret: I broke the API several times while working on this refactor. The thing is, I knew the job was well done only when the tests passed, so no regression left my own IDE.

With the tests in place, refactoring boils down to just pacifying the test gods. And they send a clear green sign to let you know you are ready:

My tests have passed. I didn’t break anything!

Summary

Our interface is a sacred contract between us and our consumers. I have written several times about the importance of covering the public interface. I can’t stretch enough about the other side of that coin – test mostly your public interface.

If you don’t test your public interface, you are probably testing the implementation detail. This is bad for various reasons, such as the coupling pitfall. It is also a cause for frustration with many developers about “changing the tests every time the implementation changes”.

Except for a very gifted few, nobody writes a perfect code the first time. A few more people make it beautiful the second time. It usually takes me three iterations to make something I’m happy with.

The ability to refactor with confidence is only achieved with tests (or with overconfidence, but that’s a different topic :)). In this example we saw how extensive tests can be for just one crucial function – and they take a second to run on every code change. You decide if you want your tests to run automatically or manually on every change you make.

What do you think? Do you make sure to distinguish between the two? Can you think of more use cases where this is important? Or maybe problematic?

Thanks a lot to Benjamin Gruenbaum and Yuval Bar Levi for the kind and thorough review.

Sign up to my newsletter to enjoy more content:

5 1 vote
Article Rating
Subscribe
Notify of
guest

0 Comments
Inline Feedbacks
View all comments