DEV Community

Cover image for Simplifying Object-Oriented Programming (OOP) Concepts with Real-Life Examples in TS
Nur Alam
Nur Alam

Posted on

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;
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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!");
Enter fullscreen mode Exit fullscreen mode

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.
Enter fullscreen mode Exit fullscreen mode

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.
Enter fullscreen mode Exit fullscreen mode

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…
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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}`);
 }
}
Enter fullscreen mode Exit fullscreen mode

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.`);
 }
}
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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");
 }
}
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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!
Enter fullscreen mode Exit fullscreen mode

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!
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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!
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Mixins

Mixins allow sharing functionality between classes without inheritance.

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

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

Imagine you’re opening a bakery!

1. Class and Object
Class: Think of a “Cake Recipe.” It has ingredients and steps but isn’t a cake yet.
Object: When you follow the recipe and bake a cake, that’s the Object!
2. Encapsulation
You have a secret frosting recipe. You keep it in a locked drawer so no one can change it. Customers can enjoy the cake but can’t access the secret recipe.
Encapsulation protects important information by hiding it.
3. Inheritance
You make a basic chocolate cake. Then, you create a “Chocolate Lava Cake” by adding some gooey chocolate inside. You didn’t start from scratch — you just extended the chocolate cake!
Inheritance lets you reuse and expand on existing ideas.
4. Polymorphism
A customer asks for a “Cake.” Some want chocolate, others want vanilla. You know how to make both, but they all just ask for a cake!
Polymorphism means one command works in different ways.
5. Abstraction
Customers only see the delicious cakes on display. They don’t need to know how the oven works or how the dough rises.
Abstraction hides complex processes and shows only the necessary parts.

Understanding SOLID Principles

1. Single Responsibility Principle (SRP)
A “CakeMaker” only handles baking cakes, not decorating them.

2. Open/Closed Principle (OCP)
You can add a new cake flavor without changing the base recipe.

3. Liskov Substitution Principle (LSP)
If a “ChocolateCake” is a type of “Cake,” customers should enjoy it just like any other cake.

4. Interface Segregation Principle (ISP)
Your bakers only need cake recipes, while cashiers only need the register instructions.

5. Dependency Inversion Principle (DIP)
Your bakery depends on suppliers for ingredients but not on which supplier it is.

Conclusion

Understanding Object-Oriented Programming (OOP) and SOLID Principles doesn’t have to be intimidating. By relating these concepts to something as simple and enjoyable as running a bakery, we can see how these principles apply to real-world scenarios. OOP helps us structure our code efficiently, making it reusable and easy to manage, while SOLID principles guide us in writing clean, scalable, and maintainable software.

Just like a bakery thrives on well-organized recipes and processes, your software projects can thrive with strong design principles. So, whether you’re baking cakes or building applications, remember that the right foundation leads to sweet success! 🍰💻

Happy Coding! 🚀

Reinvent your career. Join DEV.

It takes one minute and is worth it for your career.

Get started

Top comments (0)

AWS Security LIVE!

Tune in for AWS Security LIVE!

Join AWS Security LIVE! for expert insights and actionable tips to protect your organization and keep security teams prepared.

Learn More

👋 Kindness is contagious

Dive into an ocean of knowledge with this thought-provoking post, revered deeply within the supportive DEV Community. Developers of all levels are welcome to join and enhance our collective intelligence.

Saying a simple "thank you" can brighten someone's day. Share your gratitude in the comments below!

On DEV, sharing ideas eases our path and fortifies our community connections. Found this helpful? Sending a quick thanks to the author can be profoundly valued.

Okay