« Back to article list

Write Code Like You Write a Recipe

Table of Contents

The new requirements

Another day on the job, another day of fielding requests.

It starts innocently enough - the business needs you to write a program to automate creating peanut butter cookies (yum).

You're given the requirements (recipe) as such:

Hah! This is so easy, I could do this in my sleep.

You come up with something similar to this:

function makeCookies () {
  oven.setTemperature(350)
  sheet.grease()
  bowl.add(peanutButter)
  bowl.add(sugar)
  bowl.stir()
  bowl.beat(egg)
  bowl.beat(egg)
  bowl.stirIn([bakingSoda, salt, vanilla])
  const dough = bowl.makeDough()
  sheet.add(dough.balls())
  oven.add(sheet)
  oven.setTimer(10)
  const cookies = oven.remove(sheet)
  sheet.cool(5)

  return cookies
}

Well, that was pretty easy. Time to wrap it up for the day.

Time goes on

As it tends to do, time continues to move on. The project and code in it eventually becomes legacy code (aka, someone else's problem to maintain).

One day, the business drudges up that old automated cookie creation idea and requests that it handles a new task - "instead of just cooking peanut butter cookies, it'd be great if the cookie system could handle oatmeal raisin as well!".

Hah! You remember that old system, but you're onto newer and better things. This is a perfect task for the intern to sink their teeth into - afterall, it's legacy and not high profile, it's been stable, and it was so simple to implement the first time around.

The new set of requirements/recipe the intern works off of looks very similar to the initial ones:

He eagerly begins to expand the original code to handle the new case:

function makeCookies (type = 'peanut-butter') {
  if (type === 'peanut-butter') {
    oven.setTemperature(350)
  }

  if (type === 'oatmeal-raisin') {
    oven.setTemperature(325)
  }

  sheet.grease()

  if (type === 'peanut-butter') {
    bowl.add(peanutButter)
    bowl.add(sugar)
  }

  if (type === 'oatmeal-raisin') {
    bowl.add(oats)
    bowl.add(pumpkinPuree)
    bowl.add(sugar)
  }

  bowl.stir()

  if (type === 'peanut-butter') {
    bowl.beat(egg)
    bowl.beat(egg)
    bowl.stirIn([bakingSoda, salt, vanilla])
  }

  if (type === 'oatmeal-raisin') {
    bowl.beat(eggWhite)
    bowl.stirIn([bakingSoda, salt, cinnamon])
  }

  const dough = bowl.makeDough()
  sheet.add(dough.balls())
  oven.add(sheet)

  if (type === 'peanut-butter') {
    oven.setTimer(10)
  }

  if (type === 'oatmeal-raisin') {
    oven.setTimer(8)
  }

  const cookies = oven.remove(sheet)
  sheet.cool(5)

  return cookies
}

The code comes in, the peer reviews pass it as it meets the requirements and nothing is obviously wrong. Still, it has become a bit harder to understand and you're left with a nagging feeling.

The clean-up of the old

Some more time has passed - the intern has learned more about proper code design and modularity. They've asked to rewrite that old makeCookies function. You're a bit hesitant, as refactors may bring regressions, but you go for it.

Some time later, something similar to this appears in its place:

function makeCookies (recipe) {
  oven.setTemperature(recipe.getPreheatTemperature())
  sheet.grease()

  recipe.getPreStirItems().map(bowl.add)
  bowl.stir()

  recipe.getEggs().map(bowl.beat)
  bowl.stirIn(recipe.getPostStirItems())

  const dough = bowl.makeDough()
  sheet.add(dough.balls())
  oven.add(sheet)

  oven.setTimer(recipe.getBakeTime())

  const cookies = oven.remove(sheet)
  sheet.cool(recipe.getCoolTime())

  return cookies
}

Wow! Things really improved on this system.

The system rewrite

The legacy code is simply untenable, the implementation language and/or platform is no longer supported, and the technology choices behind it just aren't trendy enough.

It's time for a rewrite!

Well, there's one small problem…the people making up the business have shuffled, the original requirements have been lost, and the expectation is that you meet those same old non-existent requirements.

The first task is to reproduce those original requirements/recipe out of what's in the code base.

A recipe for disaster

To reproduce the requirements, the dev team has been tasked with translating the code logic to the recipe structure in the least amount of time (afterall, the underlying logic in the "recipe" object is now millions of lines of code). We need the 10,000 foot summary of what "makeCookies" actually does to create an oatmeal cookie and peanut butter cookie.

Your team comes up with something roughly like:

The Recipe to do Something With a Bowl and an Oven

  • Step 1: Preheat oven to ??? (see: secondary-doc), grease cookie sheet
  • Step 2: In bowl, stir ??? (see: secondary-doc) until smooth.
  • Step 3: Beat in ??? (see: secondary-doc).
  • Step 4: Roll dough into 1 inch balls and place 2 inches apart on the sheet.
  • Step 5: Bake for ??? minutes, cool for ???, enjoy!

Well, reading that didn't tell us much at all! In fact, all we were able to tell is that the oven gets heated, the sheet gets greased, some things are added to a bowl and out comes dough.

Ok - maybe the abstraction actually caused a complexity in understanding the code and what exactly it is doing (afterall, we're just talking about cookies, but in lots of code bases those lookup calls are not always side-effect free - they could be firing rockets).

The Recipe to Make a This or That if This or That Cookie

Yikes - second attempt - lets dig through some of the git history and see if we can do a better job building the original requirements out of the pre-refactor code:

  • Step 1: Preheat oven to 350F if peanut butter, 325F if oatmeal raisin, grease cookie sheet
  • Step 2: In bowl, stir peanut butter and sugar if PB, oats, pumpkin puree, sugar if oatmeal raisin, until smooth.
  • Step 3: Beat in 2 eggs one at a time if PB, 1 egg white if oatmeal raisin.
  • Step 4: Roll dough into 1 inch balls and place 2 inches apart on the sheet.
  • Step 5: Bake for 10 minutes if PB, 8 minutes if oatmeal raisin, cool for 5 enjoy!

Wow! Not good - I mean, slightly better, but not great. Would you try to work off a recipe like this in a cook book? Would you submit a recipe like this in a cookbook? How do you even title a recipe like this? "I call this recipe, Make Cookie!".

How could this cookie travesty have been avoided?

What if the implementation took a path that caused it to look like this:

function makePeanutButterCookies () {
  oven.setTemperature(350)
  sheet.grease()
  bowl.add(peanutButter)
  bowl.add(sugar)
  bowl.stir()
  bowl.beat(egg)
  bowl.beat(egg)
  bowl.stirIn([bakingSoda, salt, vanilla])
  const dough = bowl.makeDough()
  sheet.add(dough.balls())
  oven.add(sheet)
  oven.setTimer(10)
  const cookies = oven.remove(sheet)
  sheet.cool(5)

  return cookies
}

function makeOatmealRaisinCookies () {
  oven.setTemperature(325)
  sheet.grease()
  bowl.add(oats)
  bowl.add(pumpkinPuree)
  bowl.add(sugar)
  bowl.stir()
  bowl.beat(eggWhite)
  bowl.stirIn([bakingSoda, salt, cinnamon])
  const dough = bowl.makeDough()
  sheet.add(dough.balls())
  oven.add(sheet)
  oven.setTimer(8)
  const cookies = oven.remove(sheet)
  sheet.cool(5)

  return cookies
}

In this case, reconstructing the "what" is happening is at the forefront of the function/method. There is not a process of fill-in-the-blanks to build it out.

The total amount of cookies available (2) are all clear-cut and not hidden (yes, the OOP approach allows an infinite amount of cookies that follow this same general process, but not every cookie in the future would necessarily continue with all these steps, so is there value in having this as a single focal point/process? Is the benefit going to outweigh the clarity?)

The very first if/then approach will clearly suffer as more cookies are added over time.

The second OOP approach will suffer as soon as cookies diverge (what if they do not get built into ball shapes?)

The third approach suffers only in that on "global" updates that are not abstracted in some way, the update may need to be applied in multiple places. However this also comes with the benefit that a change to how one type of cookie is made is guaranteed to only affect that one cookie's recipe/process, and not have the potential to impact or cause regressions on hundreds of other recipes.

Comments?

Leave a comment below (or on reddit/hn).