Know when to solve a problem with a descriptive vs imperative approach

This is part of the Semicolon&Sons Code Diary - consisting of lessons learned on the job. You're in the workflows category.

Last Updated: 2024-04-25

I had some JavaScript UI code that essentially had three states:

I started off with the following code, taking the "post 2010" case as my basis:

if (yearFirstRegistered >= 2010) {
  hide("emissions_class")
// I will refer to this as the "middle step"
} else if (yearFirstRegistered == 2009) {
  show("co2_emissions")
  show("month_first_registered")
  show("emissions_class")
} else {
  hide("month_first_registered")
  hide("co2_emissions")
}

Despite being simple code, this led to inconsistent states as I cycled through settings in a random order. Why?

Ultimately this was because I took an "imperative" instead of a "descriptive" approach.

Failing 1. Assumption of passing through intermediate steps

Once I was in, for example, the "2008 and earlier" state, then when I changed the state back to "2010", this goes there so directly and does not pass though the intermediate steps. i.e. the stuff in the 2009 state ("middle step") would never happen - thus the things that were supposed to get carried forwards in time from the 2009 step (e.g. "showing the month field") would not be - leading to this field being lost when arriving to the state via this route.

Failing 2. Assumption that initial state is the master

Even if I had not made the intermediate steps assumption, the code would also be wrong. The issue was that I took a perspective that was glued to the initial state (2010), the one that was first shown when you loaded the page. I mentally diff-ed from there to the rest. This sort of reasoning would have been OK had I reset the page to initial state each time (e.g. with a page refresh). But this wasn't possible - I was dealing with a mutated DOM. Therefore it was required for me to explicitly call commands to show what (I had initially reasoned) "should always be there"

Solution 1: Rule of "Equal entries in each branch"

There are three fields to be hidden/shown. Therefore a foolproof way to ensure a mistake isn't made is to insist that each is addressed in every branch :

if (yearFirstRegistered >= 2010) {
  hideSectionAndRemoveValidations("emissions_class")
  hideSectionAndRemoveValidations("month_first_registered")

  unhideSectionAndRestoreValidations("co2_emissions")
} else if ([2008, 2009].includes(yearFirstRegistered)) {
  unhideSectionAndRestoreValidations("co2_emissions")
  unhideSectionAndRestoreValidations("month_first_registered")
  unhideSectionAndRestoreValidations("emissions_class")
} else {
  hideSectionAndRemoveValidations("month_first_registered")
  hideSectionAndRemoveValidations("co2_emissions")

  unhideSectionAndRestoreValidations("emissions_class")
}

Solution 2: Re-engineer to reset to a known starting point

The original code would have worked here (and it would have been easier to reason about)

Solution 3: Declarative representation of expectations in each state

This one is my favorite:

shown = {
  2010: ["co2"],
  2010: ["co2", "year", "emissions"],
  2010: ["emissions"],
}

// Then some code to make this happen

Lesson

Know when to take a descriptive (instead of declarative) approach to solving a problem