Object Oriented Programming - patrickcole/learning GitHub Wiki

Object Oriented Programming (OOP)

  • JavaScript added more OOP-friendly patterns and syntaxes in ES6
  • Being that JavaScript is a prototypal language, the object-oriented approach is no more than syntactic sugar (or an more familiar OOP approach) to building objects in JS
  • Built on-top of the constructor and prototype nature of JS using this context for properties
// a more traditional approach using functions:
let Meal = function(food) {
  this.food = food;
}

Meal.prototype.eat = function() {
  return 'eating...';
}

// now with classes, all properties and methods can
// be encapsulated inside a Class:
class Meal {
  constructor(food) {
    this.food = food;
  }
  eat() {
    return 'eating...';
  }
  static description() {
    return 'The Meal object brought to you by Patrick';
  }
}
  • The constructor() method represents the function that is executed on each new instantiation of the object
  • The eat() method represents a prototype method that has been added to the object Meal
  • The description() method is implemented as a static method. That means it can be called without instantiating a new object
Meal.description(); 
// => 'The Meal object brought to you by Patrick'

Instantiation / Invoking a New Object

let breakfast = new Meal('cereal');
breakfast.eat();
// => 'eating...'
breakfast.description();
// => TypeError!
  • Static methods cannot be called on an invoked or created object, they can only be called from the Class object Meal

Note: we are using the this context inside our classes. This is important to note as arrow functions use the context they are declared in. This can be difficult to manage or debug when something goes wrong. Also, declaring objects with an arrow function means they are not able to use the new keyword as well.

Static Fields

  • Fields that are able to be called directly on the class rather than on an invocation of the class object
  • They cannot be called when an object is invoked
class Computer {
  constructor(type) {
    this.type = type;
  }

  static about() {
    return 'I am a computer, beep boop'
  }
}

let mac = new Computer('Mac');
mac.about(); // TypeError!

Computer.about(); // "I am a computer, beep boop"

Private Fields

  • fields that are only available to the class itself
  • they are not available outside the class or even in subclasses (which would be protected fields)
  • private fields can be marked using the hashtag # symbol
class Machine {
  #firmware_version = "3.49.0098"
  constructor(brand) {
    this.brand = brand;
  }
}

let mac = new Machine("Apple");
console.log(mac.firmware_version); // Error!
  • this rather simple example just shows that you cannot access the private field firmware_version from outside the Class itself.
  • however, from within the class, the firmware_version is available
class Machine {
  #firmware_version = "3.49.0098"
  constructor(brand) {
    this.brand = brand;
  }
  startup(firmware_check) {
    if ( firmware_check === this.#firmware_version ) {
      return "Check matched!";
    }
  }
}

let mac = new Machine('Apple');
mac.startup("3.49.0098"); // => "Check matched!"
// but we still can access the field firmware_version:
let fw = mac.firmware_version; // => Error!, undefined

Using Extends and Super

Given the example below of student:

class Student{
  constructor(name,age){
    this.name = name;
    this.age = age;
  }
  getAge(){
    return this.age;
  }
}

A new subclass can be created extending off of the base class of Student:

class MedStudent extends Student {
  constructor(name, age, course) {
    super(name, age);
    this.course = course;
  }
  getCourse() {
    return this.course;
  }
}

// and then invoked:
let medStudent = new MedStudent('Larry', 27, 'Anatomy');

By using extends to establish the base class and then running super() inside the constructor. The extended class of MedStudent gets all the values and methods from the Student class.

Being that the MedStudent extended the Student class the prototype of MedStudent will be the Student class. Nothing changes to the Student class, it just has been used as a base for another class.

Beware of God Objects

Sometimes your classes or objects will need to share properties or methods later on during the development cycle. The natural solution might be to refactor your classes so they all inherit from what is known as a God object. However, this seemingly easy solution can potentially cause issues later on.

With inheritance, you structure your classes around what they are, a User, an Animal, a Dog, a Cat - all of those words encapsulate a meaning centered around what those things are.

Perhaps take a look at what those objects can do. In the future, these objects may change and become difficult to bootstrap new features or ideas onto them. Taking this approach is thinking of objects in more of a Composition

Composition

Rather than building dedicated objects using Class that are extended, the idea of composition takes what those objects can do. The basic rule of thumb is to develop the actions into dedicated functions that can be applied to an object, via a function constructor.

Take this example:

const eater = (state) => ({
  eat(amount) {
    console.log(`${state.name} is eating.`)
    state.energy += amount
  }
})

const sleeper = (state) => ({
  sleep(length) {
    console.log(`${state.name} is sleeping.`)
    state.energy += length
  }
})

const barker = (state) => ({
  bark() {
    console.log('Woof Woof!')
    state.energy -= .1
  }
})

We have two actions that any object can perform, regardless of what that object is. If we wanted to declare a Dog that barks in addition to eating and sleeping, we can. For the Cat object, we could just compose an object with eating and sleeping.

function Dog (name, energy, breed) {

  // using object shorthand:
  let dog = { name, energy, breed }

  // now add all these functions to the Dog object:
  return Object.assign(
    dog,
    eater(dog),
    sleeper(dog),
    barker(dog)
  )
}

// now the cat:
function Cat (name, energy, declawed) {

  // again, using object shorthand:
  let cat = { name, energy, declawed }

  return Object.assign(
    cat,
    eater(cat),
    sleeper(cat)
  )
}

// now each can still be used:
let rocky = new Dog("Rocky", 100, "Goldendoodle");
rocky.eat(10); // => "Rocky is eating"
rocky.bark(); // => "Woof Woof!"

This makes sharable actions more flexible to be added or removed in future updates. The allure of using Class to make objects inherit shared properties or methods is easy to fall under. Developers should be aware of how to plan out their objects so that they don't fall in the trap of having to refactor with God objects.


Sources