Programming Tips!

If overwhelmed, write first/code later...

  • Good programmers will write out the scenario and replace each step with actual code
    • nouns = objects
    • groups/lists/plural = arrays
    • actions = functions
    • stimulus for action (e.g. button click) = events
    • time = timers
    • choices = flow control
    • how you make that choice = expressions (age > 18)
  • If a scenario is big, break it down into smaller pieces. Don't try to do it all at once
  • Walk through the scenario in your head...
  • Don't be ashamed to google...

Code Readability

if(a<16) {
  if(p.pp) {
    serP(p)
  }
}
const AGE_THRESHOLD = 16;
const estimatedAge = 15;
const appearsUnderage = estimatedAge < AGE_THRESHOLD;
const hasID = !!(person.passort || person.drivingLicence);

if (appearsUnderage) {
  if (hasID) {
    servePerson(person)
  }
}

vs

Abstract ALL THE THINGS!!

function createTicTacToeBoard() {
  return ['', '', '', '', '', '', '', '', '']
}
function createTicTacToeBoard(rows=3, columns=3, startingValue='') {
  return new Array(rows * columns).fill(startingValue);
}

vs

  • Someone will inevitably make a 4 x 4 tic-tac-toe!
  • Set sensible defaults to allow this (see open-closed later on)
  • Try not to manually make and check things.
  • Good programmers are LAZY! Let JS do repetitive work for you

KISS: Keep It Simple, Stupid!

  • Simple code is:
    • easier to read
    • easier to maintain
    • easier to spot mistakes in
    • more efficient (usually!)

 

Don't over-think/over-engineer things! Your program should feel light and precise, not massive and overbearing!

YAGNI

  • You
  • Ain't
  • Gonna
  • Need
  • It

 

Start off with basic stuff. Don't try to build the whole model and all its methods straight away.

DRY

Don't Repeat Yourself

button.addEventListener('click', function handler(){
    doSomething(thing.property.subProperty + 1);
    doSomethingElse(thing.property.subProperty * 100);
});

button.addEventListener('mouseover', function handler(){
    doSomething(thing.property.subProperty + 1);
    doSomethingElse(thing.property.subProperty * 100);
});
function handler(){
    const value = thing.property.subProperty;
    doSomething(value + 1);
    doSomethingElse(value * 100);
}

button.addEventListener('click', handler);
button.addEventListener('mouseover', handler);

Bad

Good

'Magic Numbers' & 'Hard-coded' values

function calculatePeriod(year, animal) {
  if ((year - 1900) % 12 === 0) {
    return animal[0];
  } else if ((year - 1900) % 12 === 1) {
    return animal[1];
  } else if ((year - 1900) % 12 === 2) {
    return animal[2];
  } else if ((year - 1900) % 12 === 3) {
    return animal[3];
  } else if ((year - 1900) % 12 === 4) {
    return animal[4];
  } else if ((year - 1900) % 12 === 5) {
    return animal[5];
  } else if ((year - 1900) % 12 === 6) {
    return animal[6];
  } else if ((year - 1900) % 12 === 7) {
    return animal[7];
  } else if ((year - 1900) % 12 === 8) {
    return animal[8];
  } else if ((year - 1900) % 12 === 9) {
    return animal[9];
  } else if ((year - 1900) % 12 === 10) {
    return animal[10];
  } else if ((year - 1900) % 12 === 11) {
    return animal[11];
  }
}

Bad

  • what is gender[5] - you don't know, right?!
    • 5 is a 'magic number'
  • return 'Helio';
    • What is 'Helio'?
    • Helio is 'hard-coded' - can't be changed/overridden
  • How maintainable is the code opposite if the year becomes 1901?
  • What does it do??
  • This is sorely lacking a formula...

'Magic Numbers' & 'Hard-coded' values

const ANIMAL_CYCLE_START_YEAR = 1900;
const YEARS_IN_ONCE_CYCLE = 12;

function calculatePeriod(year) {
  const yearOfTheCycle = (year - ANIMAL_CYCLE_START_YEAR) % YEARS_IN_ONCE_CYCLE;
  return animals[yearOfTheCycle];
}

Good

  • Don't just put random numbers and text in
  • save them as variables and then use them
    • so you only need to change in one place
    • for readability, and
    • so var names show the idea behind the code

Defensive Coding

markAsLentTo(customer) {
    if (!customer instanceof Customer) { // or typeof thing !== 'string', etc.
      throw new Error(
        `Lending requries a customer. Received ${JSON.stringify(
          customer
        )} of type ${getTypeOrConstructor(customer)}`
      );
    }
    this.custodian = customer;
  }
  • When you are the person using the code it is not necessary to defend against bad values.
    • An error will be thrown and you can deal with it
  • For a team you're on it's not necessary either because they can ask you. (Also, things like typescript help too)
  • If you're writing a library (code that someone else you don't know will include in their projects then you should defensively code to ensure they're using it right and get guidance when not:

Mutability

  • Later in the course we'll look at how changing objects can make it difficult to track what's happening in an app.
  • Instead of splicing from an array we may copy parts of it (excluding the bits we don't want) and then reassign it.
  • This is for much later in the course but consider that freezing objects and setting 'read-only' values often helps.
  • All this is for later on though...

SOLID

  • Single Responsibility Principle
  • Open/Closed Principle
  • Liskov Substitution Principle
  • Interface Segregation Principle
  • Dependancy Inversion Principle

Important Note

These principles were created for class-based OOP. I have adapted them to also show how they can relate to non-class-based code.

Single Responsibility Principle

Rule: "Every module or class should have responsibility over a single part of the functionality provided by the software, and that responsibility should be entirely encapsulated by the class."

 

As Dr. Bob puts it: "A class[/function] should have only one reason to change"

 

Translation: Make your functions responsible for doing one thing. Split them down so that they can be controlled and re-used/shared more easily.

SRP Example Bad Code

function updateUI() {
    var heatingManualControl = document.getElementById("toggleHeating");
    var heatingStatusDisplay = document.getElementById("heatingStatus");
    var roomTempDisplay = document.getElementById("roomTempDisplay");
    var checkRateDisplay = document.getElementById("checkRate");
    var mercury = document.querySelector(".mercury");
    var autoHeatingControl = document.getElementById("autoHeating");

    heatingStatusDisplay.innerText = heatingOn ? "on" : "off";
    roomTempDisplay.innerText = roomTemp;
    mercury.style.height = roomTemp + "%";
    checkRateDisplay.innerText = selectedCheckRate === HEATING_CHECK_INTERVAL_FAST ? 'FAST' : 'SLOW';
}

The problem here is that:

  • we can't do things separately
  • we can't change the way/order in which they are done
  • we can't re-use any of this code

SRP Example Good Code

const heatingManualControl = document.getElementById("toggleHeating");
const heatingStatusDisplay = document.getElementById("heatingStatus");
const roomTempDisplay = document.getElementById("roomTempDisplay");
const checkRateDisplay = document.getElementById("checkRate");
const mercury = document.querySelector(".mercury");
const autoHeatingControl = document.getElementById("autoHeating");

// UI Actions
function updateUIHeatingStatus() {
  heatingStatusDisplay.innerText = heatingOn ? "on" : "off";
}

function updateRoomTempDisplay() {
  roomTempDisplay.innerText = roomTemp;
}

function updateUIThermometer() {
  mercury.style.height = roomTemp + "%";
}

function updateRateDisplay() {
  checkRateDisplay.innerText = selectedCheckRate === HEATING_CHECK_INTERVAL_FAST ? 'FAST' : 'SLOW';
}

function updateUI() {
  updateUIThermometer();
  updateRoomTempDisplay();
  updateUIHeatingStatus();
  updateRateDisplay();
}

Open/Closed Principal

Rule: "software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification"

 

Translation: You should be able to add new functionality to your code without having to re-write your existing work.

 

TL;DR: Provide defaults and allow for options to be passed

OCP Example Bad Code

function Store(products=[]) {
  let funds = 0;
  function purchase(user, item) {
      user.funds -= item.price;
      // ...
  }

  return {
    purchase: purchase
  };
}

var products = [....];
var myStore = Store(products);

Problems:

  • What if my store starts with some funds?
  • Everything is locked into the purchase method. For example:
    • How do I apply vouchers?
    • How do I install credit card purchases?

OCP Example Good Code

function Store(products=[], funds=0, purchaseMethods={}, discounts={}) {
  const defaultPurchaseMethods = {
    default(){....}
  };
  const purchaseMethods = Object.assign({}, defaultPurchaseMethods, purchaseMethods);

  function purchase(user, item, purchaseMethodName='default', discountName='') {
      if(!user) throw new Error('No user provided');
      if(!item) throw new Error('No item provided');
    
      const discount = discounts[discountName] || null;
    
      purchaseMethods[purchaseMethodName](user, item, discount);
  }

  return {
    purchase,
  };
}

// In another script:
const user = {....};
const products = [{....}, {....}];
const purchaseMethods = {....};
const discounts = {....};

const myStore = Store(products, 200, purchaseMethods, discounts);

myStore.purchase(user, 'Jumper', 'visa', 'summerSale');

Solution: Provide defaults and allow for options to be passed

Liskov Substitution Principle

Rule: "If S is a subtype of T, then objects of type T may be replaced with objects of type S (i.e. an object of type T may be substituted with any object of a subtype S) without altering any of the desirable properties of the program (correctness, task performed, etc.)"

 

Translation: If you override a method in the subclass, keep it predictable in what it returns or does as a side effect.

LSP Example Bad Code

function Boat(name) {
  this.name = name;
  this.speed = 0;
}

Boat.prototype.go = function() {
    return `${this.name} moves at ${this.speed}`; // RETURNED VALUE
}

function Powerboat() {
  Boat.apply(this, arguments);
  this.speed = 35;
}

Powerboat.prototype = Object.create(Boat.prototype);
Powerboat.prototype.constructor = Powerboat;

Powerboat.prototype.go = function() {
    console.log(`${this.name} moves at ${this.speed}`); //NO RETURNED VALUE
}

const boats = [new Boat('Boaty'), new Powerboat('vroom')];

for (const boat of boats) {
  console.log(boat.go());
}

Problem: The subclass objects go method logs the message and returns nothing, whilst the superclass returns the message.

Interface Segregation Principle

Rule: "No client should be forced to depend on methods it does not use."

 

Translation: Make sure your objects are not carrying inherited or hard-coded methods they don't need [that would only be used by other objects].

ISP Example Bad Code

function makeShape(name, width, height) {
    return {
      area(){ return width * height }
    };
}

var rectangle = makeShape('rectangle-395', 200, 300);

Problems:

  • Now a rectangle has a volume! #problem
    • worse, it will be NaN because a depth was not supplied!

Now what about 3d shapes?

function makeShape(name, width, height, depth) {
    return {
      name: name,
      area: function(){ return width * height },
      volume: function(){ return width * height * depth },
    };
}

var rectangularPrism = makeShape('rectangularPrism-203', 200, 300, 400);
var rectangle = makeShape('rectangle-395', 200, 300);

ISP Example Good Code

class Shape {
  constructor(name='', width=0, height=0,) {
    // definesive checks go here...
    this.name = name;
    this.width = width;
    this.height = height
  }
  getSurfaceArea(){
    return this.width * this.height
  }
}

class Shape3d extends Shape {
  constructor(name, width, height, depth) {
    // defensive checks again...
    super(name, width, height);
    this.depth = depth;
  }
  getSurfaceArea() {
    const { length:l, width:w, height:h } = this;
    return 2*l*w + 2*l*h + 2*h*w;
  }
  getVolume() {
    return width * height * depth;
  }
}

const rectangle = new Shape('rectangle-395', 200, 300);
const rectangularPrism = new Shape3d('rectangularPrism-203', 200, 300, 400);

Dependancy Inversion Principle

Rule:

  • High-level modules should not depend on low-level modules. Both should depend on abstractions.
  • Abstractions should not depend on details. Details should depend on abstractions.

 

Translation:

Don't let low-level details determine the range of high-level functional abilities. Aim for abstractions, not concretions - i.e. Lower-order functions (that do the actual work) should be as flexible and open as possible to being controlled by the higher-order functions (where the strategic decisions are made).

DIP Example Bad Code

// Lower order function
function makeMap(pageArea, markers) {
    new GoogleMap(pageArea, markers);
}

// Higher order function
function addMapsToListings(){
  const mapEls = document.querySelectorAll('.map');
  for (const mapEl of mapEls){
    makeMap(mapEl, { .... });
  });
}

Problems:

  • What if we want some Google maps and some Bing?
    • We can't because the lower order function has a concretion. It has GoogleMaps hard-coded into it. This implementation detail means that the control is coming from here to the higher order component, which should be doing the controlling. The dependancy has been incorrectly inverted.

DIP Example Good Code

// Lower order function
function makeMap(pageArea, mapConstructor, markers) {
    new mapConstructor(pageArea, markers);
}

// Higher order function
function addMapsToListings(){
  const mapEls = document.querySelectorAll('.map');

  for (const mapEl of mapEls) {
    const mapProvider = mapEl.dataset.mapProvider; // Bing, Google, etc
    const mapConstructor = mapProviders[mapProvider];
    makeMap(mapEl, mapConstructor, [....]);
  }
}
  • Now the lower order function does its job without any concretions.
  • It has abstractions, like mapContructor, which the higher order function provides
  • video

Big O Notation

What is it?

  • It calculates the worst case time/memory cost of your algorithm
    • (Your algorithm is the code you use to solve a problem)
  • Examples:
    • pick the nth number from an array: O(1) - constant
      • doesn't matter about array size
    • loop over an array: O(n) - linear
      • does matter about array size
    • search (where you're removing 1/2 each time) - logarithmic
    • loop over an array AND loop over each item's properties (nested loops) O(n^2) - quadratic
      • exponential growth in time!
  • Full article
  • With examples
  • Comedy Alternative Notation Tweet

Design Patterns

What? Why?

  • Design patterns are common ways to organise your code to solve everyday programming issues
    • e.g. "A third party library makes objects that you want to 'enhance' safely and dynamically' - what's the best way?" (decorator pattern)
    • or "How to make a simple interface over a complex sub-system" (facade pattern)
    • There are many books on it:
      • Addy Osmani (here) <-- this one is good/modern!
      • Stoyan Stephanov (here)
  • As JS gets better, some of these patterns become redundant (e.g. we have proxies natively in es6)
  • demos

Programming Styles and Rules

By James Sherry

Programming Styles and Rules

DRY, SOLID, Procedural, Object-oriented & functional

  • 1,583