Categories
JavaScript Best Practices

JavaScript Best Practices — Classes and Types

Spread the love

To make code easy to read and maintain, we should follow some best practices.

In this article, we’ll look at some best practices we should follow to make everyone’s lives easier.

Type Checking

We can write consistent APIs to avoid type checking.

Instead of writing:

function travelToNewYork(vehicle) {
  if (vehicle instanceof Airplane) {
    vehicle.fly(this.currentLocation, 'new york');
  } else if (vehicle instanceof Car) {
    vehicle.drive(this.currentLocation, 'new york');
  }
}

We write:

function travelToNewYork(vehicle) {
  vehicle.move(this.currentLocation, 'new york');
}

We have one move method instead of fly and drive methods.

Use Method Chaining

We can return this in our methods to make the charitable.

For instance, instead of writing:

class Cube {
  constructor(length, width, height) {
    this.length = length;
    this.width = width;
    this.height = height;
  }

  setHeight(height) {
    this.height = height;
  }

  setLength(length) {
    this.length = length;
  }

  setWidth(width) {
    this.width = width;
  }

  save() {
    console.log(this.height, this.length, this.width);
  }
}

We write:

class Cube {
  constructor(length, width, height) {
    this.length = length;
    this.width = width;
    this.height = height;
  }

  setHeight(height) {
    this.height = height;
    return this
  }

  setLength(length) {
    this.length = length;
    return this;
  }

  setWidth(width) {
    this.width = width;
    return this;
  }

  save() {
    console.log(this.make, this.model, this.color);
    return this;
  }
}

Then we can use it by writing:

new Cube(1, 2, 3).setWidth(2).setLength(4).setHeight(6);

Composition Over Inheritance

We should compose classes over inheriting them.

Subclasses aren’t as flexible as composing classes.

For instance, instead of writing:

class Employee {
  constructor(name) {
    this.name = name;
  }

  // ...
}

class EmployeePayrollData extends Employee {
  constructor(salary) {
    super();
    this.salary = salary;
  }

  // ...
}

EmployeePayrollData don’t need the content of Employee , so we shouldn’t write this.

Instead, we can write:

class EmployeePayrollData {
  constructor(salary) {
    super();
    this.salary = salary;
  }

  // ...
}

class Employee {
  constructor(name) {
    this.name = name;
  }

  setPayrollData(data){
    this.payrollData = new EmployeePayrollData(data);
  }
}

There’s no is-a relationship between the 2 classes, so there’s no need for extends .

We just use them wherever we like.

Single Responsibility Principle

Every class should have a single responsibility.

For example our Employee class only holds employee data and does employee actions:

class Employee {
  constructor(name) {
    this.name = name;
  }

  //...
}

If we want to add features that aren’t related to employees, then put them somewhere else.

Open/Closed Principle

The Open/Closed Principle means that a piece of code is open for extension and closed for modification.

This means we should be able to add new features without changing the existing code.

Making subclasses is a great way to add functionality:

class HttpRequester {
  constructor(adapter) {
    this.adapter = adapter;
  }

  fetch(url) {
    return this
      .adapter
      .request(url)
      .then(response => {
        // ...
      });
  }
}

class NodeRequester extends HttpRequester{
  constructor() {
    super();
  }

  request(url) {
    // do node request
  }
}

class AjaxRequester extends HttpRequester {
  constructor() {
    super();
    this.name = "nodeAdapter";
  }

  request(url) {
    // do ajax request
  }
}

Liskov Substitution Principle

Liskov Substitution Principle states that we should be able to substitute a parent class with a subclass and get the same result.

This means that we keep shared code in the parent class.

For instance, we write:

class Rectangle {
  constructor() {
    this.width = 0;
    this.height = 0;
  }

  setWidth(width) {
    this.width = width;
  }

  setHeight(height) {
    this.height = height;
  }

  getArea() {
    return this.width * this.height;
  }
}

class Square extends Rectangle {
  setWidth(width) {
    this.width = width;
    this.height = width;
  }

  setHeight(height) {
    this.width = height;
    this.height = height;
  }

  getArea() {
    return this.width * this.height;
  }
}

We have the Rectangle class which has the same methods as the Square class.

They share the same computations.

The only difference is that the width and height are the same in a square.

We can substitute a Rectangle with a Square instance and still set the width and height and get the area.

Conclusion

We should make our APIs consistent to avoid type checking.

We can return this in class methods to make them chainable.

Classes should have a single responsibility and subclasses should be able to be used as replacements of parent classes.

By John Au-Yeung

Web developer specializing in React, Vue, and front end development.

Leave a Reply

Your email address will not be published. Required fields are marked *