DEV Community

Ady Ngom
Ady Ngom

Posted on • Originally published at adyngom.com

[JavaScript] - How to craft intuitive and self-documenting code

As a developer you probably spend more time reading code than actually writing it. Besides your regular outputs and refactors, think about the amount of time you spend on reading tutorials, documentations, reviewing a pull request, checking the latest cool repos on Github, maintaining a project etc…, the list goes on.

Unfortunately, reading does not necessarily translate into understanding.
I’m here to bet that one the main reasons a codebase or project does not get adoption or gets abandoned is simply because it requires too much brain processing power and put simply is not intuitive.

Many in western cultures are familiar with the 5 second rule when it comes to food, I know some who count a very slow 5 in that predicament :).

When it comes to code though, John Papa rightfully states that:

If you can’t understand the code in 5 seconds, there is probably a smell, there is probably an idea that you should probably refactor.
__ John Papa — Readable Code

So to put it in perspective, let me:

  • First run you through a practical use case of a code refactor.
  • Then we can discuss the added value and benefits it could provide and finally
  • Share a few useful resources that have been very helpful in this “clean code” quest

If you are a seasoned clean coder, we hope you can help us further improve by sharing some cool tips and resources on the subject in the comments.

A “simple” user story

Given a list of hotels, please select those that are located in the US and provide the filtered list with a properly formatted phone number. Below is a sample of the list of hotels.

const hotels = [
    {
     name: "Panthers Alley",
     country: "Wakanda",
     address: "123 Best Luck Finding, 3 Rivers, Wakanda",
     phone: "5551790871"
    },
    {
     name: "The Golden Thread",
     country: "USA",
     address: "5 Maple Street, Los Angeles, CA, 90000",
     phone: "2125552345"
    },
    {
     name: "12 Crowns",
     country: "Japan",
     address: "36 Wide Alley Blvd, Tokyo",
     phone: "5558725910"
    },
    {
     name: "Petit Paris",
     country: "usa",
     address: "3669 Elm Street, New York, 30000",
     phone: "7075559087"
    },
    {
     name: "The Empress Lounge",
     country: "USA",
     address: "1 Kings Place, Atlanta, GA, 30000",
     phone: "6785553125"
    }
];

If you are able to read the above and understand without a squint what’s going on in 5 seconds or less, I bow to you 🙂

To be fair though there are a lot of good in the proposed solution such as:

  • A very descriptive choice on the naming of the constant USHotels. This implicitly indicates the type of value it will hold which is likely to be a list of hotels in the US.
  • The use of map and filter to extract and transform the result to a new set, leaving the original unchanged or in more technical terms help avoid mutating the original object.
  • Map and filter also allow for a more declarative coding style rather than the imperative that would’ve been unavoidable with the for or forEach loops.

If you want to learn more about imperative vs declarative, do yourself a favor and read this great piece from Tyler McGinnis: Imperative vs Declarative Programming

In short, declarative is more concerned about the WHAT whereas imperative is explicit on the HOW.

This point us to the first area of what might need revision in our solution — the map block is too verbose and is a deep dive into HOW to transform an unformatted number to the US standard.

In our refactoring effort we will create a set of specialized units — read functions, that will help us ease in reading and understanding at first glance but also will have the side effect of creating reusable code.

I personally call this the “Toolify” step — a cool made up word 😉 Feel free to propose something else. I’m listening.

Toolify: turn your code into small units of specialization

Let’s abstract and conquer

The map block is lengthy and will be the bulk of our refactor but let’s start with the filter step.

Let’s first create a helper function named propEquals which will only worry about if a property on an object x is equal to a value y.

Feel free to make the name more descriptive if it is not obvious to you. What is important is the name carries an implicit return type.

It’s almost obvious that it will return a boolean.

function propEquals(key, value, matchCase = false) {
 return function(obj) {
   return (!matchCase) ? obj[key] === value 
         : obj[key].toLowerCase() === value.toLowerCase();
   }
}

propEquals is generic enough to look up any key / value pair against any object. For example if we want to know if the first object in the hotels array is Wakanda, we could call it like this:

const IsWakanda = propEquals(country, Wakanda)(hotels[0]);
console.log(IsWakanda); // will output true

Now let’s add a more specific function that will only target the US countries.

Using the outer function of propEquals, we can “fix” the arguments values to always default to the US. Let’s name this new functionality isUSCountry.

const isUSCountry = propEquals.call(null, country, USA, true);

In plain English, we could say that isUSCountry derives from propEquals and set the default values of the parent function to:

  • country as key,
  • USA as value,
  • and want to select either lower or uppercase values of the string USA.

In “simpler” technical terms, isUSCountry is a composition of propEquals.

Putting it to use will give us a very nice and intuitive syntax:

const USHotels = hotels.filter(isUSCountry);
console.table(USHotels);


And below is the output of the filtered array

[
    {
      "name": "The Golden Thread",
      "country": "USA",
      "address": "5 Maple Street, Los Angeles, CA, 90000",
      "phone": "2125552345"
    },
    {
      "name": "Petit Paris",
      "country": "usa",
      "address": "3669 Elm Street, New York, 30000",
      "phone": "7075559087"
    },
    {
      "name": "The Empress Lounge",
      "country": "USA",
      "address": "1 Kings Place, Atlanta, GA, 30000",
      "phone": "6785553125"
    }
  ]


This syntax where USHotels construct is not explicitly specifying any arguments and just the function reference, is called tacit programming or point free composition

The more helpers the merrier

Now that we are done with the filter part, let’s attack the transform part or map with the same approach.

First let’s extract the phone formatting functionality an decouple it from the fact that it is always tied to an object.

We should be able to format a number whether it is part of an object or not.

So below we now have our USPhoneFormat function:

function UPhoneFormat(tel) {
    let area, prefix, line;
    area = tel.substr(0, 3);
    prefix = tel.substr(3, 3);
    line = tel.substr(6, 4);
    const formatted = `(${area}) ${prefix} ${line}`;
    return formatted;
}


Now let’s add our final helper transformProp. It will basically take a key and a transform function and apply the value transformation on a given object.

function transformProp(key, fn) {
   return function(obj) {
     return {obj, [key]: fn.call(null, obj[key]) };
   }
}


Now following the same logic we did with isUSCountry, we can “compose” a more specific functionality that only handles phone number formatting

const formatPhone = transformProp.call(null, phone, UPhoneFormat);

The big payoff

Now that we have all of our “tools” in place, we can finally revise our original syntax to just this “above the fold”:

const USHotels = hotels.filter(isUSCountry).map(formatPhone);
//add all the functions below the fold or better yet use imports 


Logging USHotels to the console will output the following:

[
 {
 name: The Golden Thread,
 country: USA,
 address: 5 Maple Street, Los Angeles, CA, 90000,
 phone: (212) 555 2345
 },
 {
 name: Petit Paris,
 country: usa,
 address: 3669 Elm Street, New York, 30000,
 phone: (707) 555 9087
 },
 {
 name: The Empress Lounge,
 country: USA,
 address: 1 Kings Place, Atlanta, GA, 30000,
 phone: (678) 555 3125
 }
]

Some might scratch their heads right now thinking “that’s an awful lot of steps to essentially get the same result. What do we gain from this?” I’m glad you asked.

The true gain can be summed up in what I called the S.C.R.E.A.M.S principle. Using it, I try to always ensure that my code is:

  • Self-documenting — hopefully this is an obvious one
  • Composable — made up word but gives you the idea
  • Readable — for human first and minified next for machines
  • Eloquent — subjective but you define what is and stick with it
  • Abstraction layer — add it wherever it makes sense
  • Maintainable — small units are easy to test
  • Scalable — separate and generic functions are easy to reuse

In closing

I have honestly started to make that extra “craft” effort in the past few months and I can admit that it was a very, very slow start.

If that is something that might be of interest to you, I suggest to start with small refactors.

Using a combination of map, filter, reduce and bind, call, apply is your Swiss-knife that can carry you a long way and is one of the routes that will help in producing “clean-code” in JavaScript.

If you are convinced, it’s now time to craft some code that S.C.R.E.A.M.S!! 🙂

A few resources

From the master himself — bookmark it!!!
Readable Code

An oldie but still a goodie
JavaScript Apply, Call and Bind Methods Are Essential for JavaScript Professionals

Can’t thank Cristian Salcescu enough for this great piece
How point-free composition will make you a better functional programmer


Hello I’m Ady Ngom. Professionally I carry the fancy title of Solutions Designer / JS Tech Lead. At the core though, I’m just a passionate about all things JavaScript and the Front-end ecosystem. Connect with me on Twitter, LinkedIn and visit my blog adyngom.com

Top comments (1)

Collapse
 
johnboy5358 profile image
John • Edited

Or, alternatively ...


const formattedHotels = (byCountry, hotelsAry) =>
  hotelsAry.reduce((selectedHotels, hotel) =>
    (hotel.country.toLowerCase() === byCountry.toLowerCase())
      ? [
          ...selectedHotels,
          {...hotel,
              phone: `(${hotel.phone.slice(0,3)}) ${hotel.phone.slice(3,6)} ${hotel.phone.slice(6)}`}
        ]
      : selectedHotels
  , []);

const usHotels = formattedHotels('usa', hotels);
console.log(usHotels);

This will probably make you squint, but it's short and sweet!

[edited: 16JUNE]

...

On reflection though, maybe, as you are talking about decomposing code as much as possible; then this might be a better solution...


// propCmp => compare the value of a property of an object using a predicate function.
// propCmp : (fn, String) -> Object -> Boolean|undefined
const propCmp = (pred, key) => obj => (key in obj) ? (pred(obj[key]) ? true : false ) : undefined

// a predicate function to determine if the value of a property is === 'usa'
const isUSA = val => (val.toLowerCase() === 'usa')

// use propCmp to see if country is === 'usa|USA'
const countryEqUSA = propCmp(isUSA, 'country')

// function to format the hotel phone no.
const formatHot = hotl => ({...hotl, phone: `(${hotl.phone.slice(0,3)}) ${hotl.phone.slice(3,6)} ${hotl.phone.slice(6)}`})

// a function to format selected hotels.
const formatHotels = (byCountry, htelsAry) =>
  htelsAry
    .filter(byCountry)
    .map(formatHot)

console.log(formatHotels(countryEqUSA, hotels))


Hope one of these two methods clicks with you.

Keep up the fp posts.

Cheers!