Simplifying Object-Oriented Programming (OOP) Concepts with Real-Life Examples in TS

What is OOP? Object-Oriented Programming (OOP) is a programming paradigm (style used to write and organize code) that organizes and models software using objects. It promotes code reusability, modularity, and scalability. Programming Paradigms: 1.Procedural Programming: It follows a step-by-step approach using functions and procedures to solve problems. Code is executed in a linear flow. Benefit: Simple and easy to understand for small programs but harder to manage as complexity grows. // C Language void greet() { printf("Hello!"); } int main() { greet(); return 0; } 2.Functional Programming: It focuses on pure functions and avoids changing data or state. Functions are treated as first-class citizens. Benefit: Improves code predictability and testability by avoiding side effects. //Javascript const add = (a, b) => a + b; console.log(add(2, 3)); // Output: 5 3.Declarative Programming: It focuses on what needs to be done rather than how to do it. The logic is handled by the underlying system. Benefit: Simplifies complex operations by abstracting logic, making code more readable. //SQL SELECT name FROM users WHERE age > 18; 4.Object-Oriented Programming (OOP): It organizes code into objects with properties and methods to model real-world entities. It promotes code reusability and modularity. Benefit: Simplifies complex systems through abstraction and makes maintenance easier with modular code. //JS class Animal { speak() { console.log("The animal makes a sound"); } } const dog = new Animal(); dog.speak(); 5.Event-Driven Programming: The program responds to user actions or system events through event handlers. It’s common in GUI and web development. Benefit: Improves user interaction by responding dynamically to events. //JS document.getElementById("btn").onclick = () => alert("Button clicked!"); Object-Oriented Programming (OOP): 1.Inheritance Inheritance allows a class (child/subclass) to acquire properties and methods from another class (parent/superclass). This helps reuse code and extend functionality. Benefit: Encourages code reusability and simplifies code maintenance. // Parent Class: Person class Person { constructor(public name: string, public age: number) {} introduce() { console.log(`Hi, I'm ${this.name} and I'm ${this.age} years old.`); } } // Child Class: Student inherits from Person class Student extends Person { constructor(name: string, age: number, public grade: string) { super(name, age); // Calls the Person constructor to set name age } study() { console.log(`${this.name} is studying in grade ${this.grade}.`); } } // Creating an instance of Student const student1 = new Student("Alice", 20, "A"); student1.introduce(); // Inherited method → Output: Hi, I'm Alice and I'm 20 years old. student1.study(); // Child's own method → Output: Alice is studying in grade A. 2.Polymorphism Polymorphism allows the same method to behave differently based on the object calling it. It helps write flexible and reusable code by using one method for many types of objects. Benefit: Increases flexibility and scalability by allowing one interface to support different data types. class Person { speak(): void { console.log("Person is speaking."); } } class Student extends Person { speak(): void { console.log("Student is studying."); } } class Teacher extends Person { speak(): void { console.log("Teacher is teaching."); } } function introduce(person: Person): void { person.speak(); } const student = new Student(); const teacher = new Teacher(); introduce(student); // Output: Student is studying. introduce(teacher); // Output: Teacher is teaching. 3.Abstraction: Abstraction in OOP hides complex implementation details and only shows essential features to the user. It simplifies the code by focusing on what an object does instead of how it does it. Benefit: Reduces complexity by focusing on high-level functionality. abstract class Animal { abstract makeSound(): void; // Abstract method (no implementation) sleep(): void { console.log("Sleeping…"); } } class Dog extends Animal { makeSound(): void { console.log("Bark!"); } } const dog = new Dog(); dog.makeSound(); // Output: Bark! dog.sleep(); // Output: Sleeping… 4.Encapsulation: Encapsulation in OOP is the process of bundling data (properties) and methods (functions) into a single unit (class) and restricting direct access to some of the object’s components. This protects the internal state of an object and only allows controlled interaction through public methods. Benefit: Protects object integrity by controlling how data is accessed and modified. class Person { private age: number; // Private property constructor(age: number) { this.age = age; } // Public method to access the private property getAge(): number { return this.age; } // Public method to modify the private property with a condition setAge(newAg

Jan 17, 2025 - 11:45
Simplifying Object-Oriented Programming (OOP) Concepts with Real-Life Examples in TS

What is OOP?

Object-Oriented Programming (OOP) is a programming paradigm (style used to write and organize code) that organizes and models software using objects. It promotes code reusability, modularity, and scalability.

Programming Paradigms:

1.Procedural Programming:
It follows a step-by-step approach using functions and procedures to solve problems. Code is executed in a linear flow.

Benefit: Simple and easy to understand for small programs but harder to manage as complexity grows.

// C Language
void greet() {
 printf("Hello!");
}
int main() {
 greet();
 return 0;
}

2.Functional Programming:
It focuses on pure functions and avoids changing data or state. Functions are treated as first-class citizens.

Benefit: Improves code predictability and testability by avoiding side effects.

//Javascript
 const add = (a, b) => a + b;
console.log(add(2, 3)); // Output: 5

3.Declarative Programming:
It focuses on what needs to be done rather than how to do it. The logic is handled by the underlying system.

Benefit: Simplifies complex operations by abstracting logic, making code more readable.

//SQL
 SELECT name FROM users WHERE age > 18;

4.Object-Oriented Programming (OOP):
It organizes code into objects with properties and methods to model real-world entities. It promotes code reusability and modularity.

Benefit: Simplifies complex systems through abstraction and makes maintenance easier with modular code.

//JS
class Animal {
 speak() {
 console.log("The animal makes a sound");
 }
}
const dog = new Animal();
dog.speak();

5.Event-Driven Programming:
The program responds to user actions or system events through event handlers. It’s common in GUI and web development.

Benefit: Improves user interaction by responding dynamically to events.

//JS
 document.getElementById("btn").onclick = () => alert("Button clicked!");

Object-Oriented Programming (OOP):

1.Inheritance

Inheritance allows a class (child/subclass) to acquire properties and methods from another class (parent/superclass). This helps reuse code and extend functionality.

Benefit: Encourages code reusability and simplifies code maintenance.

// Parent Class: Person
class Person {
 constructor(public name: string, public age: number) {}
 introduce() {
 console.log(`Hi, I'm ${this.name} and I'm ${this.age} years old.`);
 }
}
// Child Class: Student inherits from Person
class Student extends Person {
 constructor(name: string, age: number, public grade: string) {
 super(name, age); // Calls the Person constructor to set name age
 }
 study() {
 console.log(`${this.name} is studying in grade ${this.grade}.`);
 }
}
// Creating an instance of Student
const student1 = new Student("Alice", 20, "A");
student1.introduce(); // Inherited method → Output: Hi, I'm Alice and I'm 20 years old.
student1.study(); // Child's own method → Output: Alice is studying in grade A.

2.Polymorphism

Polymorphism allows the same method to behave differently based on the object calling it. It helps write flexible and reusable code by using one method for many types of objects.

Benefit: Increases flexibility and scalability by allowing one interface to support different data types.

class Person {
 speak(): void {
 console.log("Person is speaking.");
 }
}
class Student extends Person {
 speak(): void {
 console.log("Student is studying.");
 }
}
class Teacher extends Person {
 speak(): void {
 console.log("Teacher is teaching.");
 }
}
function introduce(person: Person): void {
 person.speak();
}
const student = new Student();
const teacher = new Teacher();
introduce(student); // Output: Student is studying.
introduce(teacher); // Output: Teacher is teaching.

3.Abstraction:

Abstraction in OOP hides complex implementation details and only shows essential features to the user. It simplifies the code by focusing on what an object does instead of how it does it.

Benefit: Reduces complexity by focusing on high-level functionality.

abstract class Animal {
 abstract makeSound(): void; // Abstract method (no implementation)
 sleep(): void {
 console.log("Sleeping…");
 }
}
class Dog extends Animal {
 makeSound(): void {
 console.log("Bark!");
 }
}
const dog = new Dog();
dog.makeSound(); // Output: Bark!
dog.sleep(); // Output: Sleeping…

4.Encapsulation:

Encapsulation in OOP is the process of bundling data (properties) and methods (functions) into a single unit (class) and restricting direct access to some of the object’s components. This protects the internal state of an object and only allows controlled interaction through public methods.

Benefit: Protects object integrity by controlling how data is accessed and modified.

class Person {
 private age: number; // Private property
 constructor(age: number) {
 this.age = age;
 }
 // Public method to access the private property
 getAge(): number {
 return this.age;
 }
 // Public method to modify the private property with a condition
 setAge(newAge: number): void {
 if (newAge > 0) {
 this.age = newAge;
 }
 }
}
const person = new Person(25);
console.log(person.getAge()); // Output: 25
person.setAge(30);
console.log(person.getAge()); // Output: 30
// person.age = 40; // ❌ Error: Cannot access private property

Introduction to OOP Principles (SOLID):

S: Single Responsibility Principle — A class should only have one responsibility.
Benefit: Improves code maintainability and reduces complexity by separating concerns.
Example: A User class should only handle user data, not authentication.

class User {
 constructor(public name: string, public email: string) {}
}
class AuthService {
 login(user: User) {
 console.log(`Logging in ${user.name}`);
 }
}

O: Open/Closed Principle — Classes should be open for extension but closed for modification.
Benefit: Encourages extending functionality without changing existing code, reducing the risk of bugs.
Example: Extending a payment system without changing its core logic.

interface PaymentMethod {
 pay(amount: number): void;
}
class CreditCard implements PaymentMethod {
 pay(amount: number): void {
 console.log(`Paid ${amount} with Credit Card.`);
 }
}

L: Liskov Substitution Principle — Subclasses should replace base classes without breaking functionality.
Benefit: Ensures reliability when extending classes, preventing unexpected behavior.
Example: Replacing a base Bird with a Sparrow without issues.

class Bird {
 fly(): void {
 console.log("Flying");
 }
}
class Sparrow extends Bird {}
const bird: Bird = new Sparrow();
bird.fly();

I: Interface Segregation Principle — Avoid forcing classes to implement unused methods.
Benefit: Simplifies class design and avoids unnecessary dependencies.
Example: Split interfaces for different user roles.

interface Printer {
 print(): void;
}
interface Scanner {
 scan(): void;
}
class AllInOnePrinter implements Printer, Scanner {
 print(): void {
 console.log("Printing document");
 }
 scan(): void {
 console.log("Scanning document");
 }
}

D: Dependency Inversion Principle — High-level modules should depend on abstractions, not concrete implementations.
Benefit: Promotes flexibility and makes systems easier to refactor and maintain
Example: Using interfaces instead of direct class dependencies.

interface Database {
 connect(): void;
}
class MySQL implements Database {
 connect(): void {
 console.log("Connected to MySQL");
 }
}
class App {
 constructor(private db: Database) {}
 start() {
 this.db.connect();
 }
}
const app = new App(new MySQL());
app.start();

Important Topics:

Class with Parameter Properties:

In OOP, a class is a blueprint or template for creating objects. It defines the properties (attributes) and behaviors (methods) that the objects created from the class will have.

// Without Parameter Properties
class PersonWithoutPP {
 private name: string; //Can't modify from outside
 private age: number;
 constructor(name: string, age: number) {
 this.name = name; // Manual assignment
 this.age = age;
 }
 greet(): void {
 console.log(`Hello, my name is ${this.name}.`);
 }
}
const personA = new PersonWithoutPP("Alice", 25);
personA.greet(); // Output: Hello, my name is Alice
// With Parameter Properties (Simpler)
class PersonWithPP {
 constructor(private name: string, private age: number) {} // Automatic assignment
 greet(): void {
 console.log(`Hello, my name is ${this.name}`);
 }
}
const personB = new PersonWithPP("Bob", 30);
personB.greet(); // Output: Hello, my name is Bob

Typeof and In Guard:

The typeof type guard is used to narrow down the type of a variable based on its type.

function greet(person: string | number) {
 if (typeof person === "string") {
 console.log(`Hello, ${person}!`); // If person is a string
 } else {
 console.log(`Hello, number ${person}!`); // If person is a number
 }
}
greet("Alice"); // Output: Hello, Alice!
greet(25); // Output: Hello, number 25!

The in type guard is used to check if a specific property exists in an object, helping to narrow down the type.

interface Bird {
 fly(): void;
}
interface Fish {
 swim(): void;
}
function move(animal: Bird | Fish) {
 if ("fly" in animal) {
 animal.fly(); // If it's a Bird, it will fly
 } else {
 animal.swim(); // If it's a Fish, it will swim
 }
}
const bird: Bird = { fly: () => console.log("Flying!") };
const fish: Fish = { swim: () => console.log("Swimming!") };
move(bird); // Output: Flying!
move(fish); // Output: Swimming!

InstanceOf TypeGuard:

The instanceOf operator is used to check if an object is an instance of a particular class or constructor function. It helps TypeScript narrow down the type of an object.

class Animal {
 sound() {
 console.log("Animal sound");
 }
}
class Dog extends Animal {
 sound() {
 console.log("Bark");
 }
}
class Cat extends Animal {
 sound() {
 console.log("Meow");
 }
}
function makeSound(animal: Animal) {
 if (animal instanceof Dog) {
 animal.sound(); // If it's a Dog, we call Dog's sound()
 } else if (animal instanceof Cat) {
 animal.sound(); // If it's a Cat, we call Cat's sound()
 } else {
 animal.sound(); // If it's any other Animal, we call its sound
 }
}
const myDog = new Dog();
const myCat = new Cat();
const genericAnimal = new Animal();
makeSound(myDog); // Output: Bark
makeSound(myCat); // Output: Meow
makeSound(genericAnimal); // Output: Animal sound

Access Modifier(Public, Private, Protected):

Public: Accessible anywhere.

Private: Accessible only within the class.

Protected: Accessible within the class and subclasses.

class Employee {
 // public, can be accessed anywhere
 public name: string;
 // private, can only be accessed inside this class
 private salary: number;
 // protected, can be accessed inside this class and subclasses
 protected position: string;
 constructor(name: string, salary: number, position: string) {
 this.name = name;
 this.salary = salary;
 this.position = position;
 }
 // public method
 public displayInfo(): void {
 console.log(`Name: ${this.name}, Position: ${this.position}`);
 }
 // private method
 private calculateBonus(): number {
 return this.salary * 0.1;
 }
 // protected method
 protected showSalary(): void {
 console.log(`Salary: ${this.salary}`);
 }
}
class Manager extends Employee {
 constructor(name: string, salary: number, position: string) {
 super(name, salary, position);
 }
 // Access protected method
 public displaySalary(): void {
 this.showSalary(); // Works because showSalary is protected
 }
}
const emp = new Employee("John", 50000, "Developer");
console.log(emp.name); // public access
emp.displayInfo(); // public method works
// const emp2 = new Employee("John", 50000, "Developer");
// emp2.salary; // Error: 'salary' is private
const mgr = new Manager("Alice", 70000, "Manager");
mgr.displayInfo(); // public method works
mgr.displaySalary(); // protected method works in subclass

Getter & Setter:

class Employee {
 private _name: string;
 constructor(name: string) {
 this._name = name;
 }
 // Getter for _name
 get name(): string {
 return this._name;
 }
 // Setter for _name
 set name(newName: string) {
 this._name = newName;
 }
}
const emp = new Employee("John");
console.log(emp.name); // Output: John
emp.name = "Alice";
console.log(emp.name); // Output: Alice

Statics in OOP:

Static means a property or method belongs to the class itself, not to its instances, and is shared across all objects.

class Counter {
 static count: number = 0; // Shared across all instances
 increment() {
 Counter.count++; // Increases the shared 'count'
 }
}
const obj1 = new Counter();
const obj2 = new Counter();
obj1.increment(); // Counter.count becomes 1
obj2.increment(); // Counter.count becomes 2
console.log(Counter.count); // Output: 2

Interfaces for Abstraction

Interfaces define a contract that classes must follow, ensuring consistency without implementation details.

interface Animal {
 speak(): void;
}
class Cat implements Animal {
 speak(): void {
 console.log("Meow");
 }
}
const cat = new Cat();
cat.speak(); // Output: Meow

Method Overloading

Method overloading allows a class to define multiple methods with the same name but different parameter types.

class Printer {
 print(value: string): void;
 print(value: number): void;
 print(value: any): void {
 console.log(value);
 }
}
const printer = new Printer();
printer.print("Hello"); // Output: Hello
printer.print(123); // Output: 123

Composition Over Inheritance

Composition allows building complex behavior by combining simple, reusable components rather than relying on inheritance.

class Engine {
 start() {
 console.log("Engine started");
 }
}
class Car {
 constructor(private engine: Engine) {}

 drive() {
 this.engine.start();
 console.log("Car is driving");
 }
}
const car = new Car(new Engine());
car.drive(); // Output: Engine started \n Car is driving

Decorators

Special functions that modify classes or methods.

function Log(target: any, key: string) {
 console.log(`Calling ${key}`);
}
class Person {
 @Log
 sayHello() {
 console.log("Hello!");
 }
}
const person = new Person();
person.sayHello(); 
// Output: Calling sayHello \n Hello!

Use of super Keyword

super calls methods from a parent class inside a child class.

class Animal {
 move() {
 console.log("Animal moves");
 }
}
class Dog extends Animal {
 move() {
 super.move();
 console.log("Dog runs");
 }
}
const dog = new Dog();
dog.move(); // Output: Animal moves \n Dog runs

Mixins

Mixins allow sharing functionality between classes without inheritance.

type Constructor = new (…args: any[]) => T;
function CanFly(Base: TBase) {
 return class extends Base {
 fly() {
 console.log("Flying");
 }
 };
}
class Bird {}
const FlyingBird = CanFly(Bird);
const bird = new FlyingBird();
bird.fly(); // Output: Flying

Understand OOP and SOLID with a Real-Life Story: The Bakery Business