Conquering complexity: refactoring JavaScript projects

Phil Nash

Phil Nash
Developer Advocate for Sonar

Sonar
Logos

Phil Nash

twitter.com/philnash

@philnash@mastodon.social

linkedin.com/in/philnash

https://philna.sh

Phil Nash

Part 1:

What is complexity?

Our jobs are complex

Code is inherently complex

Our job is managing complexity

The top 5 issues in all JavaScript projects

A list of top issues for JavaScript
JavaScript
1. Using "var" instead of "let" or "const"
2. Complexity of functions is too high
3. Sections of code commented out
4. "==" and "!=" instead of "===" and "!=="
5. Unused assigments

What is too high?

Part 2:

How do we measure complexity?

How complex is this?

function sumOfPrimes(max) {
  let total = 0;
  for (let i = 2; i <= max; ++i) {
    let prime = true;
    for (let j = 2; j < i; ++j) { 
      if (i % j == 0) {           
        prime = false;
      }
    }
    if (prime) {                  
      total += i;
    }
  }
  return total;
}

How complex is this?

export function getWords(number) {
  switch (number) {
    case 1:
      return "one";
    case 2:
      return "a couple";
    case 3:
      return "a few";
    case 4:
      return "many";
    default:
      return "lots";
  }
}

Cyclomatic Complexity

Cyclomatic Complexity

function sumOfPrimes(max) {        // +1
  let total = 0;
  for (let i = 2; i <= max; ++i) { // +1
    let prime = true;
    for (let j = 2; j < i; ++j) {  // +1
      if (i % j == 0) {            // +1
        prime = false;
      }
    }
    if (prime) {                   // +1
      total += i;
    }
  }
  return total;
}

Cyclomatic Complexity

export function getWords(number) { // +1
  switch (number) {
    case 1:                        // +1
      return "one";
    case 2:                        // +1
      return "a couple";
    case 3:                        // +1
      return "a few";
    case 4:                        // +1
      return "many";
    default:
      return "lots";
  }
}

Can we do better?

Cyclomatic complexity measures the number of paths through a function.

How do we measure understandability?

Cognitive Complexity

Cognitive Complexity

Creates a score by:

  • Incrementing for breaks in flow (loops/conditionals)
  • Incrementing a nesting score

Cognitive Complexity

function sumOfPrimes(max) {
  let total = 0;
  for (let i = 2; i <= max; ++i) { // +1
    let prime = true;
    for (let j = 2; j < i; ++j) {  // +2 (+1 for nesting)
      if (i % j == 0) {            // +3 (+2 for nesting)
        prime = false;
      }
    }
    if (prime) {                   // +2 (+1 for nesting)
      total += i;
    }
  }
  return total;
}

Cognitive Complexity

function getWords(number) {
  switch (number) {  // +1
    case 1: 
      return "one";
    case 2:
      return "a couple";
    case 3:
      return "a few";
    case 4:
      return "many";
    default:
      return "lots";
  }
}

Brain stack

Brain stack

for (...)

Brain stack

for (...)
for (...)

Brain stack

if (...)
for (...)
for (...)

Brain stack

for (...)
for (...)

Brain stack

for (...)

Brain stack

if (...)
for (...)

Brain stack

for (...)

Brain stack

Part 3:

Conquering complexity

1. Understand where the complexity is

Demo

2. Do nothing

Complexity is only an issue if you need to change the code

3. Clean as you code

Refactor first

Refactoring is improving how the code works, without changing what the code does

How do we know we didn't change the result?

Tests

Tests must ensure that they test what a function does, not how it does it

If you don't have tests, that is step 1 of refactoring

Tests must cover existing behaviour

Even if it's wrong

Refactor!

Reduce nesting

Invert condition and early exit

Structural collapse

Extract a helper method

Other language features

Invert condition and early exit

function onSlideTransitionEnd({ currentSlide }) {
  if ("confetti" in currentSlide.dataset) {
    const defaults = { y: 0.5 };
    if ("confettiSmall" in currentSlide.dataset) {
      // Throw a small amount of confetti
    } else if ("confettiLarge" in currentSlide.dataset) {
      // Throw a large amount of confetti
    } else {
      // etc
    }
  } 
}

Invert condition and early exit

function onSlideTransitionEnd({ currentSlide }) {
  if (!("confetti" in currentSlide.dataset)) {
    return;
  }
  const defaults = { y: 0.5 };
  if ("confettiSmall" in currentSlide.dataset) {
    // Throw a small amount of confetti
  } else if ("confettiLarge" in currentSlide.dataset) {
    // Throw a large amount of confetti
  } else {
    // etc
  }
}

Pop it off the brain stack

Structural collapse

function onSlideTransitionEnd({ currentSlide }) {
  if ("confetti" in currentSlide.dataset) {
    if (window.matchMedia(`(prefers-reduced-motion: no-preference)`).matches) {
      // Throw  confetti
    }
  } 
}

Structural collapse

function onSlideTransitionEnd({ currentSlide }) {
  if ("confetti" in currentSlide.dataset &&
      window.matchMedia(`(prefers-reduced-motion: no-preference)`).matches) {
    // Throw  confetti
  } 
}

Pop it off the brain stack

Extract a helper method

const particleCount = currentSlide.dataset["confettiParticleCount"]
    ? parseInt(currentSlide.dataset["confettiParticleCount"], 10)
    : 200;
const duration = currentSlide.dataset["confettiDuration"]
    ? parseInt(currentSlide.dataset["confettiDuration"], 10)
    : 0;

Extract a helper method

function integerDataAttrOrDefault(el, attr, def) {
  return el.dataset[attr] ? parseInt(el.dataset[attr], 10) : def;
}

// later
const particleCount = integerDataAttrOrDefault(currentSlide, "confettiParticleCount", 200);
const duration = integerDataAttrOrDefault(currentSlide, "confettiDuration", 0);

Pop it off the brain stack

Language features

const nestedProp = obj.first && obj.first.second;

Language features

const nestedProp = obj.first?.second;

Language features

const defaults = { y: 0.5 };

if (defaults.colors === undefined || defaults.colors === null) {
  defaults.colors = arrayDataAttrOrDefault(currentSlide, "confettiColors", undefined);
}

Language features

const defaults = { y: 0.5 };

defaults.colors ??= arrayDataAttrOrDefault(currentSlide, "confettiColors", undefined);

Pop it off the brain stack

Bonus rant

Nested ternaries

const animalName =
  pet.canBark() ?
    pet.isScary() ?
      'wolf'
    : 'dog'
  : pet.canMeow() ? 'cat'
  : 'probably a bunny';

Nested ternaries

function animalName(pet) {
  if (pet.canBark() && pet.isScary()) { return "wolf"; }
  if (pet.canBark()) return "dog";
  if (pet.canMeow()) return "cat";
  return "probably a bunny";
}

What is complexity?

How do we measure complexity?

Conquering complexity

Tools

SonarCloud / SonarQube

SonarLint

ESLint

eslint-plugin-sonarjs

Other links

Cognitive Complexity paper https://www.sonarsource.com/docs/CognitiveComplexity.pdf

Stop nesting ternaries https://www.sonarsource.com/blog/stop-nesting-ternaries-javascript/

🎉 Reveal.js Confetti 🎉
https://github.com/philnash/reveal-confetti

Thank you

twitter.com/philnash

@philnash@mastodon.social

linkedin.com/in/philnash

https://philna.sh

Phil Nash Sonar