Card 02/ 08

CodeDifficulty: Beginner3 min

Spotting an Open/Closed Violation

Every new shape adds an elif to the area calculator, and every elif risks the others.

An open/closed violation shows up as a type check that has to grow every time a new case is added, often repeated in several functions. This card shows a small shape calculator in Python, Java and TypeScript with two functions, area and perimeter, each of which asks what kind of shape it was given before doing anything.

Two functions that branch on the shape's type

py
import math


class Circle:
    def __init__(self, radius):
        self.radius = radius


class Rectangle:
    def __init__(self, width, height):
        self.width = width
        self.height = height


def area(shape):
    if isinstance(shape, Circle):
        return math.pi * shape.radius**2
    elif isinstance(shape, Rectangle):
        return shape.width * shape.height
    raise TypeError(f"unknown shape {type(shape).__name__}")


def perimeter(shape):
    if isinstance(shape, Circle):
        return 2 * math.pi * shape.radius
    elif isinstance(shape, Rectangle):
        return 2 * (shape.width + shape.height)
    raise TypeError(f"unknown shape {type(shape).__name__}")


for shape in [Circle(1), Rectangle(3, 4)]:
    print(f"{type(shape).__name__}: area {area(shape):.2f}, perimeter {perimeter(shape):.2f}")
OutputRan with python:3.12-slim (Python 3.12.13)
Circle: area 3.14, perimeter 6.28
Rectangle: area 12.00, perimeter 14.00
java
public class OcpShapes {
    static double area(Object shape) {
        if (shape instanceof Circle c) {
            return Math.PI * c.radius() * c.radius();
        } else if (shape instanceof Rectangle r) {
            return r.width() * r.height();
        }
        throw new IllegalArgumentException("unknown shape " + shape);
    }

    static double perimeter(Object shape) {
        if (shape instanceof Circle c) {
            return 2 * Math.PI * c.radius();
        } else if (shape instanceof Rectangle r) {
            return 2 * (r.width() + r.height());
        }
        throw new IllegalArgumentException("unknown shape " + shape);
    }

    public static void main(String[] args) {
        Object[] shapes = {new Circle(1), new Rectangle(3, 4)};
        for (Object shape : shapes) {
            System.out.printf("%s: area %.2f, perimeter %.2f%n",
                shape.getClass().getSimpleName(), area(shape), perimeter(shape));
        }
    }
}

record Circle(double radius) {}

record Rectangle(double width, double height) {}
OutputRan with eclipse-temurin:21-jdk (Java 21.0.12)
Circle: area 3.14, perimeter 6.28
Rectangle: area 12.00, perimeter 14.00
ts
class Circle {
  constructor(readonly radius: number) {}
}

class Rectangle {
  constructor(readonly width: number, readonly height: number) {}
}

function area(shape: Circle | Rectangle): number {
  if (shape instanceof Circle) {
    return Math.PI * shape.radius ** 2;
  } else {
    return shape.width * shape.height;
  }
}

function perimeter(shape: Circle | Rectangle): number {
  if (shape instanceof Circle) {
    return 2 * Math.PI * shape.radius;
  } else {
    return 2 * (shape.width + shape.height);
  }
}

for (const shape of [new Circle(1), new Rectangle(3, 4)]) {
  console.log(
    `${shape.constructor.name}: area ${area(shape).toFixed(2)}, perimeter ${perimeter(shape).toFixed(2)}`,
  );
}
OutputRan with node:22-slim (Node 22.23.1), TypeScript 5.9.3
Circle: area 3.14, perimeter 6.28
Rectangle: area 12.00, perimeter 14.00

Now add a hexagon. In the Python and Java versions, a Hexagon class alone does nothing: both area and perimeter reach their final line and raise an error, because neither knows the new type. You have to open each function and add a branch. If there were a draw and a to_json too, they would need branches as well, and missing one is only discovered when that function runs on a hexagon.

The TypeScript version fails sooner and more helpfully. Add Hexagon to the union type Circle | Rectangle and the compiler rejects both else branches, because they treat anything that is not a circle as a rectangle. The compile error is welcome, but the work is the same: every function that branches on the type has to be edited.

Those highlighted lines are the code that changes with every new case. That is the signal to look for: a conditional on a type, repeated across functions, in code that was already finished.