Applying the Open/Closed Principle in TypeScript: A Concise Overview

manas_pant

Manas

Posted on August 31, 2024

Applying the Open/Closed Principle in TypeScript: A Concise Overview

The basic definition of the principle is that software entities such as classes, modules, functions etc should be open for extension but closed for modification.
Let's break down this statement to understand it in detail.

The statement says that whenever there is a new requirement for a class, we should not be modifying the class but rather extending an abstraction of the class and utilizing it.

Let's take an example to better understand:-

Suppose we create a class for bank accounts where we have two methods to withdraw and deposit the amount.

class BankAccount {
  private _customerName: string;
  private _customerId: string;
  private _amount: number = 10000;

  constructor(customerName: string, customerId: string) {
    this._customerName = customerName;
    this._customerId = customerId;
  }

  withdrawAmount(newAmount: number) {
    this._amount = this._amount - newAmount;
  }

  depositAmount(newAmount: number) {
    this._amount = this._amount + newAmount;
  }
}
Enter fullscreen mode Exit fullscreen mode

Now we have a new requirement for another kind of bank account, and for this account, there is some additional fees for withdrawing and depositing amounts.Lets change the code accordingly.

class BankAccount {
  private _customerName: string;
  private _customerId: string;
  private _bankType: string;
  private _amount: number = 10000;

  constructor(customerName: string, customerId: string, bankType: string) {
    this._customerName = customerName;
    this._customerId = customerId;
    this._bankType = bankType;
  }

  public get amount(): number {
    return this._amount;
  }

  withdrawAmount(newAmount: number) {
    if (this._bankType === "savings") this._amount = this._amount - newAmount;
    else if (this._bankType === "current")
      this._amount = this._amount - this._amount * 0.005 - newAmount;
  }

  depositAmount(newAmount: number) {
    if (this._bankType === "savings") this._amount = this._amount + newAmount;
    else if (this._bankType === "current")
      this._amount = this._amount + this._amount * 0.005 + newAmount;
  }
}
const savingsAccount = new BankAccount("Manas", "123", "savings");
const currentAccount = new BankAccount("Manas", "123", "current");

savingsAccount.depositAmount(100000);
savingsAccount.withdrawAmount(1212);

console.log("Amount in savings account is ", savingsAccount.amount);

currentAccount.depositAmount(100000);
currentAccount.withdrawAmount(1212);

console.log("Amount in current account is ", currentAccount.amount);
Enter fullscreen mode Exit fullscreen mode

This code works, but can you find the issue with this approach?

Why Open Closed Principle

The above approach has a few issues.

  1. We will end up testing the whole functionality again since we introduced new code in an already existing feature
  2. This in turn can end up being a costly process for the organization
  3. Since our class or method might end up doing multiple things, this also breaks our Single Responsibility Principle
  4. With the addition of new code, the maintenance overhead for the classes increases.

To get rid of the above issues we use the Open Closed Principle

  1. We create an abstract class for bankAccount and then make the withdraw() and deposit() method as abstract.
  2. We then consume this abstract class for our respective bank accounts.
  3. Accordingly we also change the calculation for deposit and withdraw directly for the account type classes
abstract class BankAccount {
  private _customerName: string;
  private _customerId: string;
  private _bankType: string;
  private _amount: number = 10000;

  constructor(customerName: string, customerId: string, bankType: string) {
    this._customerName = customerName;
    this._customerId = customerId;
    this._bankType = bankType;
  }

  public get getAmount(): number {
    return this._amount;
  }

  public set setAmount(amount: number) {
    this._amount = amount;
  }

  abstract withdrawAmount(newAmount: number): void;

  abstract depositAmount(newAmount: number): void;
}

class SavingsAccount extends BankAccount {
  constructor(customerName: string, customerId: string, bankType: string) {
    super(customerName, customerId, bankType);
  }

  withdrawAmount(newAmount: number): void {
    this.setAmount = this.getAmount - newAmount;
  }

  depositAmount(newAmount: number): void {
    this.setAmount = this.getAmount + newAmount;
  }
}

class CurrentAccount extends BankAccount {
  constructor(customerName: string, customerId: string, bankType: string) {
    super(customerName, customerId, bankType);
  }

  withdrawAmount(newAmount: number): void {
    this.setAmount = this.getAmount - this.getAmount * 0.005 - newAmount;
  }

  depositAmount(newAmount: number): void {
    this.setAmount = this.getAmount + this.getAmount * 0.005 + newAmount;
  }
}

const savingsAccount = new SavingsAccount("Manas", "123", "savings");
const currentAccount = new CurrentAccount("Manas", "123", "current");

savingsAccount.depositAmount(100000);
savingsAccount.withdrawAmount(1212);

console.log("Amount in savings account is ", savingsAccount.getAmount);

currentAccount.depositAmount(100000);
currentAccount.withdrawAmount(1212);

console.log("Amount in current account is ", currentAccount.getAmount);
Enter fullscreen mode Exit fullscreen mode

From the refactored code we can see that we created new classes based on the accountType and we ended up updating the withdraw() and deposit() functions accordingly. Now if in future we get a new requirement for third type of account we can simply extend the BankAccount abstract class

💖 💪 🙅 🚩
manas_pant
Manas

Posted on August 31, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related