OOP(Object Oriented Programming)
/ 8 min read
Table of Contents
Concepts of OOP
Object-Oriented Programming (OOP) is a paradigm based on the concept of “objects,” which can contain data (properties) and code (methods).
- Class vs. Object
Class: The blueprint or template.
Object: The actual instance created from the blueprint.
// The Class (Blueprint)class Car { constructor(color, brand) { this.color = color; // Property this.brand = brand; // Property }
honk() { // Method console.log(`The ${this.color} ${this.brand} says Beep Beep!`); }}
// The Objects (Instances)const car1 = new Car("Red", "Toyota");const car2 = new Car("Blue", "Ford");
car1.honk(); // Output: The Red Toyota says Beep Beep!- The Four Pillars of OOP
A. Encapsulation
The encapsulation principle states that all important information is contained inside an object and only select information is exposed. The implementation and state of each object are privately held inside a defined class. Other objects do not have access to this class or the authority to make changes. They are only able to call a list of public functions or methods.
This characteristic of data hiding provides greater program security and avoids unintended data corruption.
Restricting direct access to some of an object’s components. In modern JavaScript, we use # for private fields.
class BankAccount { #balance; // Private field (ES2022 feature)
constructor(initialBalance) { this.#balance = initialBalance; }
deposit(amount) { if (amount > 0) { this.#balance += amount; console.log(`Deposited: $${amount}`); } }
getBalance() { return this.#balance; }}
const myAccount = new BankAccount(100);myAccount.deposit(50);console.log(myAccount.getBalance()); // Output: 150
// console.log(myAccount.#balance); // Error: Private field '#balance' must not be accessedB. Abstraction
Objects only reveal internal mechanisms that are relevant for the use of other objects, hiding any unnecessary implementation code. The derived class can have its functionality extended. This concept can help developers more easily make additional changes or additions over time.
Hiding complex implementation details. JavaScript doesn’t have an abstract keyword, but we can simulate it by throwing errors if a method isn’t implemented by a child.
class Shape { constructor() { if (this.constructor === Shape) { throw new Error("Abstract class 'Shape' cannot be instantiated directly."); } }
calculateArea() { throw new Error("Method 'calculateArea()' must be implemented."); }}
class Circle extends Shape { constructor(radius) { super(); this.radius = radius; }
calculateArea() { return 3.14 * this.radius * this.radius; }}
// const shape = new Shape(); // Throws Errorconst circle = new Circle(5);console.log(circle.calculateArea()); // Output: 78.5C. Inheritance
Classes can reuse code and properties from other classes. Relationships and subclasses between objects can be assigned, enabling developers to reuse common logic, while still maintaining a unique hierarchy.
Inheritance forces more thorough data analysis, reduces development time and ensures a higher level of accuracy.
A mechanism where a child class derives properties and methods from a parent class using extends.
class Animal { eat() { console.log("I am eating..."); }}
class Dog extends Animal { bark() { console.log("Woof!"); }}
const myDog = new Dog();myDog.eat(); // Inherited from AnimalmyDog.bark(); // Specific to DogD. Polymorphism
Objects are designed to share behaviors, and they can take on more than one form. The program determines which meaning or usage is necessary for each execution of that object from a parent class, reducing the need to duplicate code.
A child class is then created, which extends the functionality of the parent class. Polymorphism enables different types of objects to pass through the same interface.
Objects of different classes can be treated as objects of a common superclass. In JS, this often means different classes simply implementing the same method name.
class Cat { makeSound() { return "Meow"; }}
class Dog { makeSound() { return "Woof"; }}
function animalSound(animal) { // The function doesn't care if it's a Cat or Dog, // as long as it has a .makeSound() method. console.log(animal.makeSound());}
animalSound(new Cat()); // Output: MeowanimalSound(new Dog()); // Output: WoofSOLID Principles
A. Single Responsibility Principle (SRP)
“A class should have one, and only one, reason to change.”
In simple terms, a class should do one thing and do it well. If your User class is handling database connections, sending emails, and validating passwords, it’s doing too much.
Benefit: Easier testing and fewer side effects when making changes.
// --- VIOLATION ---class User { constructor(name) { this.name = name; }
saveToDB() { // User class shouldn't handle DB logic console.log(`Saving ${this.name} to database...`); }}
// --- CORRECTION (SRP) ---class User { constructor(name) { this.name = name; }}
class UserRepository { save(user) { console.log(`Saving ${user.name} to database...`); }}B. Open/Closed Principle (OCP)
“Software entities should be open for extension, but closed for modification.”
You should be able to add new functionality without touching the existing, working code. You achieve this through inheritance or interfaces.
Example: If you have a PaymentProcessor, don’t keep adding if/else statements for “PayPal” or “Stripe.” Instead, create a Payment interface and let new classes implement it.
class Shape { area() { throw new Error("Not implemented"); }}
class Rectangle extends Shape { constructor(w, h) { super(); this.w = w; this.h = h; } area() { return this.w * this.h; }}
class Circle extends Shape { constructor(r) { super(); this.r = r; } area() { return 3.14 * this.r * this.r; }}
// Usagefunction printTotalArea(shapes) { // We can add new shapes (Triangles, etc.) without changing this function code let total = 0; shapes.forEach(shape => total += shape.area()); console.log("Total Area:", total);}C. Liskov Substitution Principle (LSP)
“Subtypes must be substitutable for their base types.”
If you have a parent class and a child class, you should be able to swap the parent for the child without the program breaking.
The Classic “Square-Rectangle” Problem:
A Square is mathematically a Rectangle, but if your Rectangle class allows you to set width and height independently, and your Square class forces them to be equal, substituting a Square where a Rectangle is expected might break your logic.
class Bird { eat() { console.log("Eating..."); }}
class FlyingBird extends Bird { fly() { console.log("Flying..."); }}
class Sparrow extends FlyingBird { /* OK */ }
class Penguin extends Bird { // A Penguin is a Bird, but NOT a FlyingBird. // If we made Penguin extend FlyingBird, calling .fly() would be logically wrong.}D. Interface Segregation Principle (ISP)
“Clients should not be forced to depend on methods they do not use.”
It is better to have many small, specific interfaces than one massive, “fat” interface.
Example: Instead of a Worker interface with Work() and Eat(), create IWorkable and IEatable. That way, a Robot class isn’t forced to implement an Eat() method it can’t use.
// --- VIOLATION (Conceptually) ---// If we had a "Machine" class with print(), scan(), and fax(),// a simple printer would be forced to have scan() and fax() methods even if empty.
// --- CORRECTION (Composition over Inheritance) ---const printerMixin = { print() { console.log("Printing..."); }};
const scannerMixin = { scan() { console.log("Scanning..."); }};
class MultiFunctionMachine { // Uses both capabilities}Object.assign(MultiFunctionMachine.prototype, printerMixin, scannerMixin);
class SimplePrinter { // Only uses print capability}Object.assign(SimplePrinter.prototype, printerMixin);E. Dependency Inversion Principle (DIP)
“Depend on abstractions, not concretions.”
High-level modules shouldn’t depend on low-level modules; both should depend on interfaces. This “decouples” your code.
Analogy: You don’t solder your lamp directly into the electrical wiring of your house. You use a plug (the interface). This allows you to swap the lamp (the low-level module) without rewriting the house’s electrical system (the high-level module).
Dependency Injection
Dependency Injection is a technique to implement Inversion of Control (IoC).
Inversion of Control (IoC) is a design principle in software engineering where the control flow of a program is reversed compared to traditional procedural programming. Instead of the application code directly controlling the execution sequence and managing dependencies, this responsibility is transferred to a framework, container, or external service.
The Concept: Instead of a class creating the objects it depends on (its dependencies) inside itself, those objects are “injected” into the class from the outside (usually via the constructor, a property, or a method).
The Problem it Solves: If Class A creates an instance of Class B using new ClassB(), Class A is “tightly coupled” to Class B. You cannot easily swap Class B for a different implementation or a mock version for testing.
Example:
Without DI: A Car class creates a GasEngine inside its constructor.
With DI: The Car class asks for an IEngine interface in its constructor. You can pass in a GasEngine, ElectricEngine, or MockEngine.
Benefit: dramatically improves testability and flexibility.
Singleton (Design Pattern)
The Singleton is a “Creational” design pattern.
The Concept: It ensures that a class has only one instance and provides a global point of access to that instance.
Implementation: It typically involves a private constructor (so no one else can create it) and a static property that holds the single instance.
Common Use Cases: Database connections, Logging services, Configuration managers.
The Controversy: Singletons are often considered an “Anti-Pattern” because:
- They act like global variables (state can change unpredictably).
- They hide dependencies (you don’t see them in the constructor).
- They make unit testing difficult (hard to reset state between tests).
Coupling
Definition: The degree of interdependence between software modules.
Goal: Loose Coupling.
Explanation: Tight Coupling: If Class A changes, Class B breaks. This is bad. It creates a “Ripple Effect” of bugs.
Loose Coupling: Class A and Class B communicate via simple interfaces. You can change the internals of A without affecting B.
Cohesion
Definition: The degree to which the elements inside a module (or class) belong together.
Goal: High Cohesion.
Explanation:
High Cohesion: A class named UserManager contains methods like Login, Logout, and UpdatePassword. Everything is related.
Low Cohesion: A class named Utility contains CalculateTax, SendEmail, and ResizeImage. These are unrelated concepts randomly thrown together.
The Golden Rule: You generally want Low Coupling and High Cohesion.