OOP and the 4 principles of OOP

It is a programming pattern where code is written and managed through objects. Objects can be considered as a block of code that contains data and certain methods which can manipulate that data. Objects are created from classes. Classes can be considered as a blueprint of an object, which means that classes define all the properties and methods that an object created from that class can have.

Objects can have 2 main types of elements - properties and methods. Properties are just simple key-value pairs and they are primarily used to store data. Methods are functions that we use to manipulate the data in the object. Let's understand this with an example -

class Employee {
  constructor(fullName, salary, designation) {
    this.fullName = fullName;
    this.salary = salary;
    this.designation = designation;
  }

  _summarize() {
    console.log(
      `${this.fullName} works as a ${this.designation} and earns ${this.salary} LPA`
    );
  }

  static _message() {
    console.log("This is the message from class");
  }
}

const emp1 = new Employee("Gaurav", 1000000, "Software Engineer"); 
emp1._summarize();
Employee._message();

So, every object made from this class must have 3 properties - fullName, salary and designation. And we have two methods in this class - summarize and message. But as you can see the message method has been set to static, which means that it can be accessed only from the class and not from the objects made from this class. However, the method summarize can be accessed from the objects made from this class.

Now, let's understand the 4 main principles of OOP

Encapsulation

Encapsulation means protecting certain properties and methods of a class from being modified by something outside the class. Some methods and properties are specific to that class only and they should not be manipulated by anything outside that class or else, it might affect the code of that class. Such methods and properties are made “private” to that class. Consider the code given below -

class Employee {
  #department; // private/protected property

  constructor(fullName, salary, designation) {
    this.fullName = fullName;
    this.salary = salary;
    this.designation = designation;
    this.#department = "Engineering";
  }

  _summarize() {
    console.log(
      `${this.fullName} works as a ${this.designation} and earns ${this.salary} LPA`
    );
  }

  _shortSummarize() {
    console.log(`${this.fullName} works in ${this.#department} department.`);
  }

  #_modifySalary(newSalary) { // private/protected method
    this.salary = newSalary;
    console.log(`${this.fullName}'s salary is changed to ${this.salary}`);
  }

  changeSalary(newSalary) {
    this.#_modifySalary(newSalary);
  }

  static _message() {
    console.log("This is the message from class");
  }
}

const emp1 = new Employee("Gaurav", 1200000, "Software Engineer");
console.log(emp1.#department); // shows error
emp1._shortSummarize();

emp1.#_modifySalary(20000); // shows error
emp1.changeSalary(1600000);

In this example, #department and #_modifySalary are private to the class Employee and they can be accessed from within the Employee class only. These properties and methods cannot be accessed even from the objects made by this class.

Inheritance

Inheritance refers to a situation when one class “inherits” properties and methods from another class. The class which inherits is called the child class and the class from which properties or methods are inherited is called the parent class. Inheritance is a very good mechanism for avoiding code repetition. Sometimes, multiple classes can have the same properties or methods. So, instead of writing those properties/methods for every class, we can make a parent class and let other classes inherit those properties or methods from that parent class. And of course, child classes can have their own properties and methods. Let me demonstrate this with the help of code -

class Employee {
  #department;

  constructor(fullName, salary, designation) {
    this.fullName = fullName;
    this.salary = salary;
    this.designation = designation;
    this.#department = "Engineering";
  }

  _summarize() {
    console.log(
      `${this.fullName} works as a ${this.designation} and earns ${this.salary} LPA`
    );
  }

  _shortSummarize() {
    console.log(`${this.fullName} works in ${this.#department} department.`);
  }

  #_modifySalary(newSalary) {
    this.salary = newSalary;
    console.log(`${this.fullName}'s salary is changed to ${this.salary}`);
  }

  changeSalary(newSalary) {
    this.#_modifySalary(newSalary);
  }

  static _message() {
    console.log("This is the message from class");
  }
}

class SoftwareEngineer extends Employee {
  constructor(fullName, salary, designation, expertise, experience) {
    super(fullName, salary, designation); // properties of the parent class

    // Properties of SoftwareEngineer class
    this.expertise = expertise; 
    this.experience = experience;
  }

  _detailedSummary() { // a method of SoftwareEngineer class
    this._summarize(); // a method declared in the parent class - Employee
    console.log(
      `${this.fullName} is an expert at ${this.expertise} and has ${this.experience} years of experience.`
    );
  }
}

const SE1 = new SoftwareEngineer(
  "Gaurav",
  2000000,
  "Senior Software Engineer",
  "Backend",
  5
);
SE1._detailedSummary();

In this example, the SoftwareEngineer class inherits properties and methods from its parent class Employee. This is done by using the keyword extends. Now, all the properties and methods (except private properties and methods) of the Employee class can be accessed in the SoftwareEngineer class. As you can see, _detailedSummary is a method in SoftwareEngineer class but it utilizes the summarize method of its parent class.

Polymorphism

Polymorphism is when a child class overwrites the properties or methods inherited from the parent class. So, we are creating different versions of the parent class. In fact, the word polymorphism itself means multiple shapes or forms.

class Employee {
  #department;

  constructor(fullName, salary, designation) {
    this.fullName = fullName;
    this.salary = salary;
    this.designation = designation;
    this.#department = "Engineering";
  }

  _summarize() {
    console.log(
      `${this.fullName} works as a ${this.designation} and earns ${this.salary} LPA`
    );
  }

  _shortSummarize() {
    console.log(`${this.fullName} works in ${this.#department} department.`);
  }

  #_modifySalary(newSalary) {
    this.salary = newSalary;
    console.log(`${this.fullName}'s salary is changed to ${this.salary}`);
  }

  changeSalary(newSalary) {
    this.#_modifySalary(newSalary);
  }

  static _message() {
    console.log("This is the message from class");
  }
}

class SoftwareEngineer extends Employee {
  constructor(fullName, salary, designation, expertise, experience) {
    super(fullName, salary, designation);
    this.expertise = expertise;
    this.experience = experience;
  }

  changeSalary(newSalary) { // overriding the method from the parent class
    this.salary = newSalary;
  }
}

const SE2 = new SoftwareEngineer(
  "Ravi",
  1200000,
  "Frontend Engineer",
  "Frontend",
  3
);
SE2.changeSalary(1200012); 
console.log(SE2.salary);

As you can see in this example, the changeSalary method has been re-defined in the child class and it has different functionality. Means it has overridden the method of its parent class and we have made a different version of the parent class Employee.

Abstraction

Every class has properties and methods which are required for its internal working but it is not needed to expose all of them. In fact, some properties or methods should strictly be confined within that class only. The idea of exposing only the relevant details of a class for users is called abstraction.

For example, the dashboard of a car shows only limited information about the car's state like fuel level, speed, engine heating level and a few others. However, in reality, there are many more pieces of information about the car’s state but they are not relevant to the user, hence they are not shown on the dashboard.