Card 03/ 08

WalkthroughDifficulty: Beginner4 min

Refactoring to Open/Closed, Step by Step

Introduce the interface, move each case into a class, then delete the branch.

Refactoring to open/closed means replacing a conditional on a type with polymorphism: each case becomes a class that knows its own behaviour, and the caller calls the method without asking which case it has. This card runs that process on the shape calculator, whose area and perimeter functions branch on circle and rectangle, in the order that keeps the output unchanged at every step.

Replacing the shape conditionals with a Shape interface

Pin the output

Run the calculator and keep what it prints for a circle of radius 1 and a 3 by 4 rectangle: areas 3.14 and 12.00, perimeters 6.28 and 14.00. Every later step must print exactly this.

Introduce the interface

Declare a Shape interface with the methods that vary by case, area() and perimeter(). In Python that is an abstract base class; in Java and TypeScript an interface. Nothing uses it yet, so the output cannot change.

Move each branch into a class

Make Circle implement Shape, and move the body of the circle branch of area into Circle.area. Do the same for perimeter, then for Rectangle. Make the old functions call these methods inside their branches, and run again.

Make the caller depend on the interface

Write the caller against Shape: a describe(shape) that calls shape.area() and shape.perimeter() and never mentions a concrete class. Run again.

Delete the conditional

The old area and perimeter functions now only forward to the methods, so delete them, branches and all. Run one last time.

The order matters for the same reason in every refactoring: deleting the conditional first leaves nothing that works, and writing the classes before the interface leaves nothing for the caller to depend on.

Shapes after the refactoring, with a hexagon added

Here is the final code with the test the refactoring was for: a Hexagon, written after the refactoring, as a new class only.

The shapes after the refactoring, with a hexagon added as a new class

py
import math
from abc import ABC, abstractmethod


class Shape(ABC):
    @abstractmethod
    def area(self): ...

    @abstractmethod
    def perimeter(self): ...


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

    def area(self):
        return math.pi * self.radius**2

    def perimeter(self):
        return 2 * math.pi * self.radius


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

    def area(self):
        return self.width * self.height

    def perimeter(self):
        return 2 * (self.width + self.height)


class Hexagon(Shape):
    def __init__(self, side):
        self.side = side

    def area(self):
        return 3 * math.sqrt(3) / 2 * self.side**2

    def perimeter(self):
        return 6 * self.side


def describe(shape: Shape):
    return f"{type(shape).__name__}: area {shape.area():.2f}, perimeter {shape.perimeter():.2f}"


for shape in [Circle(1), Rectangle(3, 4), Hexagon(2)]:
    print(describe(shape))
OutputRan with python:3.12-slim (Python 3.12.13)
Circle: area 3.14, perimeter 6.28
Rectangle: area 12.00, perimeter 14.00
Hexagon: area 10.39, perimeter 12.00
java
public class OcpShapesOpen {
    static String describe(Shape shape) {
        return String.format("%s: area %.2f, perimeter %.2f",
            shape.getClass().getSimpleName(), shape.area(), shape.perimeter());
    }

    public static void main(String[] args) {
        Shape[] shapes = {new Circle(1), new Rectangle(3, 4), new Hexagon(2)};
        for (Shape shape : shapes) {
            System.out.println(describe(shape));
        }
    }
}

interface Shape {
    double area();

    double perimeter();
}

record Circle(double radius) implements Shape {
    public double area() { return Math.PI * radius * radius; }

    public double perimeter() { return 2 * Math.PI * radius; }
}

record Rectangle(double width, double height) implements Shape {
    public double area() { return width * height; }

    public double perimeter() { return 2 * (width + height); }
}

record Hexagon(double side) implements Shape {
    public double area() { return 3 * Math.sqrt(3) / 2 * side * side; }

    public double perimeter() { return 6 * side; }
}
OutputRan with eclipse-temurin:21-jdk (Java 21.0.12)
Circle: area 3.14, perimeter 6.28
Rectangle: area 12.00, perimeter 14.00
Hexagon: area 10.39, perimeter 12.00
ts
interface Shape {
  area(): number;
  perimeter(): number;
}

class Circle implements Shape {
  constructor(readonly radius: number) {}
  area(): number { return Math.PI * this.radius ** 2; }
  perimeter(): number { return 2 * Math.PI * this.radius; }
}

class Rectangle implements Shape {
  constructor(readonly width: number, readonly height: number) {}
  area(): number { return this.width * this.height; }
  perimeter(): number { return 2 * (this.width + this.height); }
}

class Hexagon implements Shape {
  constructor(readonly side: number) {}
  area(): number { return ((3 * Math.sqrt(3)) / 2) * this.side ** 2; }
  perimeter(): number { return 6 * this.side; }
}

function describe(shape: Shape): string {
  return `${shape.constructor.name}: area ${shape.area().toFixed(2)}, perimeter ${shape.perimeter().toFixed(2)}`;
}

for (const shape of [new Circle(1), new Rectangle(3, 4), new Hexagon(2)]) {
  console.log(describe(shape));
}
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
Hexagon: area 10.39, perimeter 12.00

The first two lines of output match the pinned output exactly. The third line comes from a class that did not exist when describe was written, and describe was not opened to add it. That is the principle working: the calculating code is closed, and the set of shapes is open.