Practical Clean Code

BarcelonaJS - 2018-10-22

Israel Saeta Pérez (@dukebody)

TravelPerk

mission:

manage complexity

projects delay/failure

sneaky bugs

developer suffering & quit

contagious mess

What is clean code

  • reads like prose
  • makes it hard for bugs to hide
  • can be enhanced by a developer other than its original author
  • provides one obvious way to make things

1. the prose-programming process

  • Step away from your keyboard
  • Write the logic you want to implement in
    ​a paper
  • Write for non-technical users
  • Use multiple different levels of detail until you get to something you believe you can implement

 

  • Implement each different piece of logic as a separate function

can a person buy beer?

a person can buy beer if it is at least 18 years old

 

  


to compute the age of a person from its birthdate

take the difference from current year and its birthdate year

remove one if its birthday has not yet passed this year

the birthday of a person has passed if the month of its birthday is larger or equal to current month, and the day of its birthday is larger or equal that the current day

can a person buy beer?

function canBuyBeer(person) {
    return person.age >= 18;
}


class Person:
    get age() {
      const birthdate = this.birthdate;
      const now = Date();
      const year_diff = now.getFullYear() - birthdate.getFullYear();
    
      if (this.hasAnniversaryPassed(now, birthdate)) {
        return year_diff;
      } else {
        return year_diff - 1;
      }
} 

function hasAnniversaryPassed(now, anniversary) {
    return now.getMonth() >= anniversary.getMonth() 
            && now.getDate() > anniversary.getDate();
}

2. use meaningful variable and function names

  • Avoid too short names in general (<= 2 chars)
  • Avoid too generic names like stuff, tmp, list, do, process, etc.
  • Take into account the variable/function scope
  • If it is difficult to name, it might be a bad idea
// bad
let ret = [];
for (const l of legs) {
    const s = l.getFirstSegment();
    ret.push(s);
}
return ret;


// better
let firstSegments = []
for (const leg of legs) {
    const segment = leg.getFirstSegment();
    firstSegments.push(segment);
}
return firstSegments;
// bad, too generic variable names
function getPopularity() {
    let popularity = {}

    allPopularity = db.open("popularity_db.csv")
    for (aLine of allPopularity) {
        let tmp = aLine.split("\t")
        if (tmp.length === 2) {
            popularity[tmp[0]] = int(tmp[1].trim())
        }
    }
    return popularity
}


// better
function getPopularityByAirport() {
    let popularities = {}

    popularityDB = db.open("popularity_db.csv")

    for (line of popularityDB) {
        let values = line.split("\t")

        let correctLine = values.length === 2
        if (correctLine) {
            airportCode = values[0]
            airportPopularity = parseInt(values[1].trim())
            popularities[airportCode] = airportPopularity
        }
    }
    return popularities
}

3. break long nested functions into smaller ones

reduce cognitive complexity

detect duplicate trips

Given a trip, its duplicates are those from the same company that have the same signature

// bad
function getDuplicatedTrips(trip) {
    // calculate signature for trip
    let signature = '';
    for (const leg of trip.legs) {
        for (const segment of leg.segments) {
            signature += getSignature(segment);
        }
    }

    // get trips candidate for being duplicate
    const company = trip.company;
    const candidateTrips = db.trips.filter(company=company);

    // calculate signature for each trip
    let candidateSignatures = [];
    for (const candidateTrip of candidateTrips) {
        let candidateSignature = '';
        for (const leg of candidateTrip.legs) {
            for (const segment of leg.segments) {
                candidateSignature += getSignature(segment);
            }
        }
        candidateSignatures.push(candidateSignature);
    }

    // get the trips for which the signature matches
    let duplicatedTrips = [];
    for (let i = 0; i < candidateSignatures.length; i++) {
        let candidateSignature = candidateSingatures[i];
        if (candidatSsignature === signature) {
            duplicatedTrips.push(candidateTrips[i]);
        }
    }

    return duplicatedTrips;
}

DETECT duplicated trips

// better
function getDuplicatedTrips(trip) {
    const signature = getTripSignature(trip);
    const candidateTrips = getCandidateTrips(trip);
    return candidateTrips.filter(trip => getTripSignature(trip) === signature);
}


function getTripSignature(trip) {
    let signature = '';
    for (const leg of trip.legs) {
        for (const segment of leg.segments) {
            signature += getSignature(segment);
        }
    }
    return signature;
}


function getCandidateTrips(trip) {
    const company = trip.company;
    return db.trips.filter(company=company);
}

DETECT duplicated trips

COMPUTE TOTAL INVOICE AMOUNTS FOR MONTH

Get all invoices for the given month and add up their amounts

// bad
function get_total_invoice_amounts(year, month) {
  // get inovices for given month
  const start = Date(year, month, 1);
  const next_month = month != 12 ? month + 1 : 1;
  const end = Date(year, next_month, 1);
  const invoices = db.invoices.filter({
    created_at__gte: start,
    created_at__lte: end
  });

  // sum amounts for invoices
  let total = 0;
  for (const invoice of invoices) {
    for (const item of invoice.items) {
      total += item.amount;
    }
  }
  return total;
}

Compute total invoice amounts for month

// better
function get_total_invoice_amounts(year, month) {
  invoices = get_invoices_for_month(year, month);
  amounts = invoices.map(invoice => get_amount(invoice));
  return sum(amounts);
}

function get_invoices_for_month(year, month) {
  const start = Date(year, month, 1);
  const next_month = month !== 12 ? month + 1 : 1;
  const end = Date(year, next_month, 1);
  return db.invoices.filter({ created_at__gte: start, created_at__lte: end });
}

function get_amount(invoice) {
  // should be internal to class invoice
  const amounts = invoice.items.map(item => item.amount);
  return sum(amounts);
}

function sum(elements) {
  return elements.reduce((element_1, element_2) => element_1 + element_2, 0);
}

Compute total invoice amounts for month

clarify conditionals

// OK
if (['create', 'update', 'delete'].includes(action)) {
    check_permission(user, 'write')
}


// better
is_write_action = ['create', 'update', 'delete'].includes(action)
if (is_write_action) {
    check_permission(user, 'write')
}


// even better
function is_write_action(action) {
    return ['create', 'update', 'delete'].includes(action)
}

if (is_write_action(action)) {
    check_permission(user, 'write')
}

4. hide details safely

with abstraction and encapsulation

abstraction

Manage complexity by separating the (public) object interfaces from their (hidden) implementation details

encapsulation

Bind together data and operations, allowing only operation through certain methods to prevent misuse

Instead of accessing objects attributes directly, create business methods and use only those, transferring defensiveness from the caller to the callee

// bad
// caller needs to know internals
for (const leg of dog.legs) {
    leg.move(10)
}



// better
dog.move(10)

class Dog {
    move(forward) {
        for (const leg of self.legs) {
            leg.move(forward)
        }
    }
}

ask dog move forward

(hide internal structure)

// bad
function getTotalInvoicesAmount(invoices) {
    let total = 0
    for (const invoice of invoices) {
        // exposes internal structure!
        for (const item in invoice.items) {
            total += item.amount
        }
    }
    return total
}


// better
class Invoice {
    get amount() {
        return sum(this.items.map(item => item.amount))
    }
}

function getTotalInvoicesAmount(invoices) {
    let total = 0
    for (const invoice of invoices) {
        total += invoice.amount
    }
    return total
}

hide internal structure

// bad
function pay(product, wallet) {
    // touches internal data!
    wallet.money -= product.price
}


// better
function pay(product, wallet) {
    wallet.take(product.price)
}


class Wallet {
    take(price) {
        if price > this.money:
            throw Error('Not enough money')
        // ...
    }
}

avoid impossible states

(negative money amount)

// risky
const price = {
    'amount': 13,
    'currency': 'EUR'
}


// better
class Price {
     constructor(amount, currency):
        if (this.amount < 0) {
            throw Error('Amount cannot be smaller than zero')
        }
        this.amount = amount

        if (!ALLOWED_CURRENCIES.includes(currency)) {
            throw Error(`Unknown currency ${currency}`)
        }
        this.currency = currency
    }
}


const price = new Price(13, 'EUR')

avoid impossible states

(negative amount, invlaid currency)

6. start simple and do continous refactoring

  • Avoid premature flexibility or features because You Arent Gonna Need It
  • Keep the design short and simple (KISS)
  • Avoid premature abstraction: duplication is far better than the worng abstraction (wait for three examples)
  • Easy to refactor to add features later if needed
  • Scout rule: Always leave code cleaner than you found it

References

thanks! :)

Practical Clean Code - JS

By Israel Saeta Pérez

Practical Clean Code - JS

  • 1,961