What are the principles of OOP?

👨‍💻 Frontend Developer 🟠 May come up 🎚️ Hard
#OOP #Theory

Quick Answer

Object-Oriented Programming (OOP) is a programming paradigm based on four core principles:

  1. Encapsulation: Hiding the internal implementation of an object and providing access to its state only through public methods. Data and the methods that operate on it are bundled together into a single component — an object.
  2. Inheritance: A mechanism that allows one class (child) to inherit properties and methods from another class (parent). This promotes code reuse.
  3. Polymorphism: The ability of objects of different classes to respond to the same method call in different ways. This allows for a single interface to be used to work with objects of different types.
  4. Abstraction: Highlighting the most significant characteristics of an object and ignoring secondary details. The user is provided with only the necessary functionality, while the complex implementation is hidden.

Detailed Breakdown with Examples

1. Encapsulation

Encapsulation is the bundling of data and the methods for processing it within a single object, as well as protecting that data from external interference. Access to the data is controlled through public methods (getters and setters).

In JavaScript, encapsulation can be implemented using classes and private fields (using #).

Example:

class User {
  #email; // Private field
 
  constructor(name, email) {
    this.name = name;
    this.#email = email;
  }
 
  // Public method to get the email (getter)
  getEmail() {
    return this.#email;
  }
 
  // Public method to change the email (setter)
  setEmail(newEmail) {
    if (newEmail.includes('@')) {
      this.#email = newEmail;
    } else {
      console.error('Error: invalid email');
    }
  }
 
  getInfo() {
    // Internal methods can work with private fields
    return `User: ${this.name}, Email: ${this.#email}`;
  }
}
 
const user = new User('John', 'john@test.com');
console.log(user.name); // 'John'
// console.log(user.#email); // Error! No access to the private field
 
console.log(user.getEmail()); // 'john@test.com'
user.setEmail('new-email@test.com');
console.log(user.getEmail()); // 'new-email@test.com'
user.setEmail('invalid-email'); // Error: invalid email

2. Inheritance

Inheritance allows a new class to be created based on an existing one, inheriting its properties and methods. This promotes code reuse and the creation of a class hierarchy.

Example:

class Animal {
  constructor(name) {
    this.name = name;
  }
 
  makeSound() {
    console.log('Animal makes a sound');
  }
}
 
// The Dog class inherits from Animal
class Dog extends Animal {
  constructor(name, breed) {
    super(name); // Call the parent class constructor
    this.breed = breed;
  }
 
  // Overriding the parent method
  makeSound() {
    console.log('Dog barks: Woof-woof!');
  }
 
  // Dog's own method
  wagTail() {
    console.log(`${this.name} wags its tail.`);
  }
}
 
const myDog = new Dog('Rex', 'Shepherd');
myDog.makeSound(); // 'Dog barks: Woof-woof!'
myDog.wagTail(); // 'Rex wags its tail.'
console.log(myDog.name); // 'Rex' (inherited from Animal)

3. Polymorphism

Polymorphism (from Greek for “many forms”) is the ability of a function or method to process data of different types. In the context of OOP, this means that objects of different classes can react differently to the same method call.

In the example above, polymorphism is demonstrated by the makeSound() method existing in both the Animal and Dog classes, but working differently.

Example:

class Cat extends Animal {
  makeSound() {
    console.log('Cat meows: Meow!');
  }
}
 
const animals = [
  new Animal('Creature'),
  new Dog('Buddy', 'Mutt'),
  new Cat('Whiskers', 'Siamese')
];
 
// A single interface to work with different objects
function printSound(animalList) {
  for (const animal of animalList) {
    animal.makeSound();
  }
}
 
printSound(animals);
// Output:
// Animal makes a sound
// Dog barks: Woof-woof!
// Cat meows: Meow!

Here, we call the same makeSound() method on different objects, and each one executes its own implementation.

4. Abstraction

Abstraction is the hiding of complex logic and providing a simple interface for interaction. The user doesn’t need to know how a method works, only what it does.

Classes themselves are a form of abstraction. When we create an object, we work with its methods without thinking about their internal implementation.

Example:

Imagine a complex object, like a CoffeeMachine.

class CoffeeMachine {
  #waterLevel; // Hidden details
 
  constructor() {
    this.#waterLevel = 0;
  }
 
  #heatWater() {
    console.log('Heating water...');
  }
 
  #grindCoffee() {
    console.log('Grinding beans...');
  }
 
  // Public method - a simple abstraction
  makeEspresso() {
    this.#grindCoffee();
    this.#heatWater();
    console.log('Your espresso is ready!');
  }
}
 
const machine = new CoffeeMachine();
machine.makeEspresso(); // We just call one method
 
// We don't need to know about #heatWater() or #grindCoffee()
// All the complexity is hidden behind a simple interface.

The user simply calls makeEspresso(), and the coffee machine does everything itself. This is abstraction — providing a simple interface to perform a complex task.