DEV Community

Cover image for SOLID: O - Open/Closed Principle (OCP)
Paulo Messias
Paulo Messias

Posted on

SOLID: O - Open/Closed Principle (OCP)

Introduction to OCP:
The Open/Closed Principle (OCP) is another key component of the SOLID principles. It states that software entities (classes, modules, functions, etc.) should be open for extension but closed for modification. This means that you should be able to add new functionality to a class or module without changing its existing code. Adhering to this principle helps in building a system that is robust, maintainable, and scalable.

Objectives of OCP:

  • Promote Reusability: Existing code remains unchanged and reusable.
  • Enhance Flexibility: New features can be added with minimal impact on existing code.
  • Improve Maintainability: Reduces the risk of introducing bugs when new functionality is added.
  • Facilitate Scalability: Makes the system easier to scale as new requirements emerge.

Bad Practice Example (Classes):
Here, we have a Shape class that calculates the area of different shapes. Every time a new shape is added, the existing Shape class needs to be modified.

class Shape {
  type: string;

  constructor(type: string) {
    this.type = type;
  }

  calculateArea(): number {
    if (this.type === 'circle') {
      // Logic to calculate area of circle
      return Math.PI * 3 * 3;
    } else if (this.type === 'square') {
      // Logic to calculate area of square
      return 4 * 4;
    }
    // More shapes...
    return 0;
  }
}
Enter fullscreen mode Exit fullscreen mode

In this approach, the Shape class needs to be modified every time a new shape type is introduced, violating the OCP.

Good Practice Example (Classes):
To follow OCP, we can use polymorphism to extend the functionality without modifying existing code.

abstract class Shape {
  abstract calculateArea(): number;
}

class Circle extends Shape {
  radius: number;

  constructor(radius: number) {
    super();
    this.radius = radius;
  }

  calculateArea(): number {
    return Math.PI * this.radius * this.radius;
  }
}

class Square extends Shape {
  side: number;

  constructor(side: number) {
    super();
    this.side = side;
  }

  calculateArea(): number {
    return this.side * this.side;
  }
}

// Adding a new shape
class Rectangle extends Shape {
  width: number;
  height: number;

  constructor(width: number, height: number) {
    super();
    this.width = width;
    this.height = height;
  }

  calculateArea(): number {
    return this.width * this.height;
  }
}
Enter fullscreen mode Exit fullscreen mode

Now, if we need to add a new shape, we can simply create a new class that extends Shape without modifying existing classes.

Bad Practice Example (Functions):
Here, we have a function that processes different types of payments. Each time a new payment type is added, the function needs to be modified.

function processPayment(paymentType: string, amount: number): void {
  if (paymentType === 'creditCard') {
    // Process credit card payment
  } else if (paymentType === 'paypal') {
    // Process PayPal payment
  }
  // More payment types...
}
Enter fullscreen mode Exit fullscreen mode

In this approach, the function needs to be modified every time a new payment type is introduced, violating the OCP.

Good Practice Example (Functions):
To follow OCP, we can use a strategy pattern to extend the functionality without modifying the existing function.

interface PaymentProcessor {
  process(amount: number): void;
}

class CreditCardPayment implements PaymentProcessor {
  process(amount: number): void {
    // Process credit card payment
  }
}

class PayPalPayment implements PaymentProcessor {
  process(amount: number): void {
    // Process PayPal payment
  }
}

function processPayment(paymentProcessor: PaymentProcessor, amount: number): void {
  paymentProcessor.process(amount);
}

// Usage
const creditCardPayment = new CreditCardPayment();
processPayment(creditCardPayment, 100);

const paypalPayment = new PayPalPayment();
processPayment(paypalPayment, 200);
Enter fullscreen mode Exit fullscreen mode

This way, we can add new payment processors without modifying the existing processPayment function.

Application in React Native with TypeScript:
Imagine we are developing a task management app. We can apply OCP by extending functionality through additional classes or functions without modifying existing code.

Bad Practice Example (Classes):

class TaskService {
  addTask(task: Task): void {
    // Logic to add task
  }

  removeTask(taskId: string): void {
    // Logic to remove task
  }

  notifyTaskDue(taskId: string): void {
    // Logic to notify that the task is due
  }

  // Adding a new method directly violates OCP
  exportTasks(format: string): void {
    if (format === 'csv') {
      // Logic to export tasks in CSV
    } else if (format === 'json') {
      // Logic to export tasks in JSON
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Good Practice Example (Classes):

class TaskService {
  addTask(task: Task): void {
    // Logic to add task
  }

  removeTask(taskId: string): void {
    // Logic to remove task
  }
}

class TaskNotificationService {
  notifyTaskDue(taskId: string): void {
    // Logic to notify that the task is due
  }
}

interface TaskExporter {
  export(tasks: Task[]): void;
}

class CSVTaskExporter implements TaskExporter {
  export(tasks: Task[]): void {
    // Logic to export tasks in CSV format
  }
}

class JSONTaskExporter implements TaskExporter {
  export(tasks: Task[]): void {
    // Logic to export tasks in JSON format
  }
}

// Usage
const tasks: Task[] = [/*...*/];
const csvExporter = new CSVTaskExporter();
csvExporter.export(tasks);

const jsonExporter = new JSONTaskExporter();
jsonExporter.export(tasks);
Enter fullscreen mode Exit fullscreen mode

Bad Practice Example (Functions):

function addTaskAndExport(task: Task, format: string): void {
  // Logic to add task
  // Logic to export task in given format
}
Enter fullscreen mode Exit fullscreen mode

Good Practice Example (Functions):

function addTask(task: Task): void {
  // Logic to add task
}

function exportTasks(tasks: Task[], exporter: TaskExporter): void {
  exporter.export(tasks);
}

// Using the separated functions
const tasks: Task[] = [/*...*/];
addTask(newTask);

const csvExporter = new CSVTaskExporter();
exportTasks(tasks, csvExporter);

const jsonExporter = new JSONTaskExporter();
exportTasks(tasks, jsonExporter);
Enter fullscreen mode Exit fullscreen mode

By extending functionality through separate classes and functions, we maintain the OCP, making the application more robust and easier to maintain.

Conclusion:
Following the Open/Closed Principle helps keep the code base stable and flexible. By designing your system to be open for extension but closed for modification, you can add new features with minimal risk of introducing bugs. This principle is particularly valuable in React Native development with TypeScript, ensuring that your applications can grow and adapt over time while maintaining a clean and manageable code base.

Top comments (0)