SOLID - Open-Closed Principle

Solid Principles

In this tutorial we are going to learn about the Open–Closed Principle (OCP).

Table of contents

GitHub repository

Reference

  • S Single Responsibility Principle
  • O Open-Closed Principle
  • L Liskov Substitution Principle
  • I Interface Segregation Principle
  • D Dependency Inversion Principle

Definition

Open–Closed Principle (OCP) states that a class should be open for extension but closed for modification.

The open part of this principle implies that the class should be easily extendable. The closed part of this principle implies that the class should not be modified once it is ready.

Example

Consider the following Java program. We have a AreaCalculator class which computes the area of different shapes based on their types and then adds them together to return the total area.

import java.util.List;

class Square {
  private final double side;

  public Square(double side) {
    this.side = side;
  }

  public double getSide() {
    return side;
  }
}


class Rectangle {
  private final double length;
  private final double width;

  public Rectangle(double length, double width) {
    this.length = length;
    this.width = width;
  }

  public double getLength() {
    return length;
  }

  public double getWidth() {
    return width;
  }
}


class Triangle {
  private final double base;
  private final double height;

  public Triangle(double base, double height) {
    this.base = base;
    this.height = height;
  }

  public double getBase() {
    return base;
  }

  public double getHeight() {
    return height;
  }
}


class AreaCalculator {
  private final List<Object> shapes;

  public AreaCalculator(List<Object> shapes) {
    this.shapes = shapes;
  }

  public double totalArea() {
    double total = 0;
    for(Object shape: shapes) {
      if (shape instanceof Square) {
        Square square = (Square) shape;
        total += square.getSide() * square.getSide();
      }
      else if (shape instanceof Rectangle) {
        Rectangle rectangle = (Rectangle) shape;
        total += rectangle.getLength() * rectangle.getWidth();
      }
      else if (shape instanceof Triangle) {
        Triangle triangle = (Triangle) shape;
        total += 0.5 * triangle.getBase() * triangle.getHeight();
      }
    }
    return total;
  }
}

Test

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

import java.util.ArrayList;

public class AreaCalculatorTests {
  @Test
  @DisplayName("Total Area")
  public void shouldBeAbleToReturnTotalAreaOfAllTheShapes() {
    ArrayList<Object> shapes = new ArrayList<>();

    Square square = new Square(10);
    shapes.add(square);
    Rectangle rectangle = new Rectangle(10, 20);
    shapes.add(rectangle);
    Triangle triangle = new Triangle(5, 10);
    shapes.add(triangle);

    Assertions.assertEquals(new AreaCalculator(shapes).totalArea(), 325);
  }
}

Problem

In the current implementation of the AreaCalculator class we are computing the area of different shapes after checking the type of the shape using if-else statements. If we want to add new shapes like Circle, Octagon, etc. then we would have to open this class and make modifications and this would clearly break the Open-Closed Principle.

Solution

We can make the AreaCalculator class Open-Closed compliant by moving the area computation logic inside the respective classes. For this we can create a Shape interface which will get implemented by the different shape classes like Square, Triangle, etc.

import java.util.List;

interface Shape {
  public double getArea();
}


class Square implements Shape {
  private final double side;

  public Square(double side) {
    this.side = side;
  }

  public double getArea() {
    return side * side;
  }
}


class Rectangle implements Shape {
  private final double length;
  private final double width;

  public Rectangle(double length, double width) {
    this.length = length;
    this.width = width;
  }

  public double getArea() {
    return length * width;
  }
}


class Triangle implements Shape {
  private final double base;
  private final double height;

  public Triangle(double base, double height) {
    this.base = base;
    this.height = height;
  }

  public double getArea() {
    return 0.5 * base * height;
  }
}


class AreaCalculator {
  private final List<Shape> shapes;

  public AreaCalculator(List<Shape> shapes) {
    this.shapes = shapes;
  }

  public double totalArea() {
    double total = 0;
    for(Shape shape : shapes) {
      total += shape.getArea();
    }
    return total;
  }
}

Test

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

import java.util.ArrayList;

public class AreaCalculatorTests {
  @Test
  @DisplayName("Total Area")
  public void shouldBeAbleToReturnTotalAreaOfAllTheShapes() {
    ArrayList<Shape> shapes = new ArrayList<>();

    Square square = new Square(10);
    shapes.add(square);
    Rectangle rectangle = new Rectangle(10, 20);
    shapes.add(rectangle);
    Triangle triangle = new Triangle(5, 10);
    shapes.add(triangle);

    Assertions.assertEquals(new AreaCalculator(shapes).totalArea(), 325);
  }
}