Latest Post: Integrating Large Language Models into Frontends

The O in SOLID: Open/Close Principle

Enhance code flexibility, maintainability, and scalability by with the Open/Close Principle

5 min read
The letters SOLID written in big letters and then a very short description of each principle: Single Responsibility Principle, Open/Closed Principle, Liskov Substitution Principle, Interface Segregation Principle, Dependency Inversion Principle

Welcome to the second part of the SOLID principles series! If you missed the first part, you can check it out here:

The S in SOLID: Single Responsibility Principle

In this article, we will explore the Open/Closed Principle, the O in SOLID. Let’s dive in!

Open/Close Principle: The O in SOLID

The Open/Closed Principle (OCP) states that software entities (classes, modules, functions, etc.) should be open for extension but closed for modification. This means that the behavior of a module can be extended without modifying its source code. It helps make systems more flexible and easier to maintain over time by reducing the impact of changes.

As I did in the previous part, I will provide you with some cool, easy examples to understand the Open/Closed Principle in action. We will start with a simple example in Vanilla JavaScript or TypeScript and then move on to a more complex example in React.

Example: Open/Close Principle in Vanilla JavaScript or TypeScript

Imagine that you have a Shape class, and you need to calculate the area of different shapes (circle, square, etc.). Instead of modifying the class each time a new shape is added, we can extend the class to handle new shapes.

// Base Shape interface
interface Shape {
  area(): number;
}

// Circle class, implementing Shape
class Circle implements Shape {
  constructor(private radius: number) {}

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

// Square class, implementing Shape
class Square implements Shape {
  constructor(private side: number) {}

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

// Now you can easily extend the system with new shapes without modifying existing classes
class Rectangle implements Shape {
  constructor(private width: number, private height: number) {}

  area(): number {
    return this.width * this.height;
  }
}

So we can just now do the following:

const circle = new Circle(5)
console.log(circle.area()) // 78.54

const square = new Square(5)
console.log(square.area()) // 25

const rectangle = new Rectangle(5, 10)
console.log(rectangle.area()) // 50

They all implement the Shape interface, and we can easily add new shapes without modifying the existing classes. And make it really easy to add new shapes without modifying the existing code.

// Adding a Triangle class implementing Shape
class Triangle implements Shape {
  constructor(private base: number, private height: number) {}

  area(): number {
    return (this.base * this.height) / 2;
  }
}

// Create a triangle with base 6 and height 4
const triangle: Shape = new Triangle(6, 4);
console.log(triangle.area()); // 12

Another example, maybe more practical for a web application could be a tax calculator. We can have a base tax calculator and then extend it for different countries.

// Initial function for tax calculation
type TaxCalculator = (amount: number) => number;

const calculateUSTax: TaxCalculator = (amount) => amount * 0.07;

const calculateEUTax: TaxCalculator = (amount) => amount * 0.2;

const calculateTax = (amount: number, calculator: TaxCalculator) => {
  return calculator(amount);
};

Then we can use it like this:

// Define the amount
const amount = 100

// Calculate tax for the US using the US tax calculator
const usTax = calculateTax(amount, calculateUSTax)
console.log(`Tax for the US: $${usTax}`) // Output: Tax for the US: $7

// Calculate tax for the EU using the EU tax calculator
const euTax = calculateTax(amount, calculateEUTax)
console.log(`Tax for the EU: €${euTax}`) // Output: Tax for the EU: €20

If later we can to reach also Canada, we can just add a new function and use it in the calculateTax function.

const calculateCanadaTax: TaxCalculator = (amount) => amount * 0.05;

const canadaTax = calculateTax(amount, calculateCanadaTax);
console.log(`Tax for Canada: $${canadaTax}`); // Output: Tax for Canada: $5

Let us see now how we can apply the Open/Close Principle in a React application.

Example: Open/Close Principle in React: Extending Components Using Composition

Imagine that you have a Button component that you want to extend to create a PrimaryButton and a SecondaryButton. Instead of modifying the Button component, you can extend it using composition.

type ButtonProps = {
  label: string;
  onClick: () => void;
};

const Button: React.FC<ButtonProps> = ({ label, onClick }) => (
  <button onClick={onClick}>{label}</button>
);

const PrimaryButton: React.FC<ButtonProps> = (props) => (
  <Button {...props} />
);

const SecondaryButton: React.FC<ButtonProps> = (props) => (
  <Button {...props} />
);

Here, PrimaryButton and SecondaryButton can be created without modifying the original Button component, adhering to OCP.

Another example could be to extend functionality using higher-order components (HOCs). Imagine that you have a withLogger HOC that logs the props of a component. You can extend the functionality of a component without modifying it by wrapping it with the HOC.

import React from 'react';

type ButtonProps = {
  label: string;
  onClick: () => void;
};

// Base button component
const Button: React.FC<ButtonProps> = ({ label, onClick }) => (
  <button onClick={onClick}>{label}</button>
);

// Higher Order Component for logging clicks
const withLogging = (Component: React.FC<ButtonProps>): React.FC<ButtonProps> => {
  return (props) => {
    const handleClick = () => {
      console.log(`Button clicked: ${props.label}`);
      props.onClick();
    };

    return <Component {...props} onClick={handleClick} />;
  };
};

const LoggingButton = withLogging(Button);

In this example, instead of modifying the Button component to add logging, we create an HOC withLogging to add the functionality. This follows the Open/Closed Principle because the original component is not modified.

Conclusion

The Open/Closed Principle is a fundamental principle in software development that helps make systems more flexible and easier to maintain over time. By adhering to this principle, you can enhance code flexibility, maintainability, and scalability. I hope this article has helped you understand the Open/Closed Principle better and how to apply it in your projects. It might be hard to distinguish between OCP and Liskov Substitution Principle, so we will cover that in the next article of the series. Stay tuned!

Here are other articles that might be interesting for you

FAQ about Open/Close Principle


Share article