Open/Closed Principle In Typescript
Tags: oop, typescript
Published: 2023-10-12 12:11:54

## 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.

```ts
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.

```ts
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.

```ts
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.

```ts
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

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

### Step 2: Create a class for each communication channel

```ts
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.

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

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

```ts
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.

```ts
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.

```ts
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

```ts
interface Shape {
    area(): number;
}
```

### Step 2: Create a class for each shape

```ts
class SquareShape implements Shape {
    public constructor(public side: number) {}

    public area() {
        return this.side * this.side;
    }
}
```

```ts
class CircleShape implements Shape {
    public constructor(public radius: number) {}

    public area() {
        return Math.PI * Math.pow(this.radius, 2);
    }
}
```

```ts
class RectangleShape implements Shape {
    public constructor(public width: number, public height: number) {}

    public area() {
        return this.width * this.height;
    }
}
```

```ts
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

```ts
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.

## Following the list

You can see the updated list of design principles from the following link
https://mentoor.io/en/posts/634524154/open-closed-principle-in-typescript

Join us in our Discord Community
https://discord.gg/XDZcTuU8c8