Open/Closed Principle In Typescript

Hasan Zohdy
Oct 12, 2023
3 min read
post_comment0 Comments
post_like0 Likes

#Introduction

Open/Closed Principle is the second principle in SOLID principles, it states that, a software entity should be open for extension, but closed for modification.

#What is a software entity?

A software entity is a class, module, function, etc. Basically, it is a piece of code that does something.

#What does that mean?

Basically, you should be able to extend the behavior of a software entity without modifying its source code.

#Why would i need that?

Actually, for many reasons, let's list them:

  1. Maintainable,Your code will be more maintainable, because you will not need to modify the source code of the software entity.
  2. Testable, as it does not require modification, you will not need to modify the tests.
  3. Reusable, you can use the software entity in other places which can be easily extended.
  4. Scalable, add more features or functions without the need to modify the original source code.

#Enough Talking... Show me the code

Let's say we have a User class, and we want to add a new feature to it, which is sendEmail function, which sends an email to the user.

class User { constructor(public name: string, public email: string) {} sendEmail() { console.log(`Sending email to ${this.name} at ${this.email}`); } }

Now, we want to add a new feature to the User class, which is sendSMS function, which sends an SMS to the user.

class User { constructor(public name: string, public email: string) {} sendEmail() { console.log(`Sending email to ${this.name} at ${this.email}`); } sendSMS() { console.log(`Sending SMS to ${this.name} at ${this.email}`); } }

Now, we want to add a new feature to the User class, which is sendPushNotification function, which sends a push notification to the user.

class User { constructor(public name: string, public email: string) {} sendEmail() { console.log(`Sending email to ${this.name} at ${this.email}`); } sendSMS() { console.log(`Sending SMS to ${this.name} at ${this.email}`); } sendPushNotification() { console.log(`Sending Push Notification to ${this.name} at ${this.email}`); } }

Now, we want to add a new feature to the User class, which is sendWhatsAppMessage function, which sends a WhatsApp message to the user.

class User { constructor(public name: string, public email: string) {} sendEmail() { console.log(`Sending email to ${this.name} at ${this.email}`); } sendSMS() { console.log(`Sending SMS to ${this.name} at ${this.email}`); } sendPushNotification() { console.log(`Sending Push Notification to ${this.name} at ${this.email}`); } sendWhatsAppMessage() { console.log(`Sending WhatsApp Message to ${this.name} at ${this.email}`); } }

This is good, but we still have to modify the User class every time we want to add a new feature, which is not totally good, because we are modifying the User class and make it over complicated.

Now let's head to the Open/Closed Principle solution.

#Open/Closed Principle Solution

The idea is simple, we will make a contract A.K.A interface, which will be used among variant classes, and each class will implement the contract, and add its own implementation.

The point here is to make the contract as generic as possible, to allow multiple communication channels without touching the original User class.

#Step 1: Create a contract

interface CommunicationChannel { send(user: User): void; }

#Step 2: Create a class for each communication channel

class EmailChannel implements CommunicationChannel { send(user: User) { console.log(`Sending email to ${user.name} at ${user.email}`); } }

Now we can add as many communication channels as we want, without touching the User class.

class SMSChannel implements CommunicationChannel { send(user: User) { console.log(`Sending SMS to ${user.name} at ${user.email}`); } }
class PushNotificationChannel implements CommunicationChannel { send(user: User) { console.log(`Sending Push Notification to ${user.name} at ${user.email}`); } }
class WhatsAppChannel implements CommunicationChannel { send(user: User) { console.log(`Sending WhatsApp Message to ${user.name} at ${user.email}`); } }

#Area Calculator Example

Let's take another example, we have a Shape and an AreaCalculator classes, and we want to add a new shape to the AreaCalculator class.

class Shape { constructor(public type: string) {} } class AreaCalculator { constructor(public shapes: Shape[]) {} area() { return this.shapes.reduce((sum, shape) => { if (shape.type === "circle") { sum += Math.PI * Math.pow(shape.radius, 2); } else if (shape.type === "square") { sum += Math.pow(shape.side, 2); } else if (shape.type === "rectangle") { sum += shape.width * shape.height; } return sum; }, 0); } }

What if we want to add a new shape, let's say triangle, we will have to modify the AreaCalculator class, which is not good.

class Shape { constructor(public type: string) {} } class AreaCalculator { constructor(public shapes: Shape[]) {} area() { return this.shapes.reduce((sum, shape) => { if (shape.type === "circle") { sum += Math.PI * Math.pow(shape.radius, 2); } else if (shape.type === "square") { sum += Math.pow(shape.side, 2); } else if (shape.type === "rectangle") { sum += shape.width * shape.height; } else if (shape.type === "triangle") { sum += (shape.base * shape.height) / 2; } return sum; }, 0); } }

TOO BIG, is'nt it? let's break it down.

#Step 1: Create a contract

interface Shape { area(): number; }

#Step 2: Create a class for each shape

class SquareShape implements Shape { public constructor(public side: number) {} public area() { return this.side * this.side; } }
class CircleShape implements Shape { public constructor(public radius: number) {} public area() { return Math.PI * Math.pow(this.radius, 2); } }
class RectangleShape implements Shape { public constructor(public width: number, public height: number) {} public area() { return this.width * this.height; } }
class TriangleShape implements Shape { public constructor(public base: number, public height: number) {} public area() { return (this.base * this.height) / 2; } }

#Step 3: Use the contract

class AreaCalculator { public constructor(public shapes: Shape[]) {} public sum() { return this.shapes.reduce((total, shape) => { return total + shape.area(); }, 0); } }

Clean, clear, easy to read and maintainable!

#Conclusion

Open/Closed principle is very essential and crucial when you build your software architecture, it will make your code more maintainable, testable, reusable and scalable.

You are not logged in.