Card 03/ 08
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.
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
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))Circle: area 3.14, perimeter 6.28
Rectangle: area 12.00, perimeter 14.00
Hexagon: area 10.39, perimeter 12.00public 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; }
}Circle: area 3.14, perimeter 6.28
Rectangle: area 12.00, perimeter 14.00
Hexagon: area 10.39, perimeter 12.00interface 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));
}Circle: area 3.14, perimeter 6.28
Rectangle: area 12.00, perimeter 14.00
Hexagon: area 10.39, perimeter 12.00The 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.
← → move through sections and cards