Beginner16 min readSOLID Principleslive prototype

Open/Closed Principle (OCP)

The 'O' in SOLID: your code should be open for extension but closed for modification — you add new behaviour by writing a new class, not by editing (and risking) the code that already works.

The idea

What it is

The Open/Closed Principle is the O in SOLID. It says: software should be open for extension but closed for modification. In plain words — when a new requirement shows up, you should be able to add new behaviour by writing new code, without going back and editing the code that already works.

Think of the power sockets in your house. When you buy a new lamp, a toaster, or a phone charger, you just plug it in. You don't rewire the wall, and you don't risk breaking the fridge that's already plugged in next to it. The socket is a fixed, stable contract; any appliance that fits the plug works. Your code wants the same shape: a stable socket (an interface) that new things plug into, so adding a toaster never means cutting into the wall.

The one sentence to remember

Open for extension, closed for modification — add new behaviour by writing a new class that plugs into a stable interface, instead of editing old, tested code.

Mechanics

How it works

The smell: a switch you have to edit every single time

OCP is easiest to understand by its violation. Picture an AreaCalculator that computes the area of shapes. A beginner writes one method with a big switch (or a chain of if/else) on the shape's type:

  • if (shape.type === "circle") → use the circle formula.
  • else if (shape.type === "rectangle") → use the rectangle formula.
  • else if (shape.type === "triangle") → use the triangle formula.

It works today. But what happens when product asks for a hexagon? You have to open up AreaCalculator and add another branch. The same thing happens for a discount calculator that switches on customer type (new, regular, vip), or a renderer that switches on file type. Every new variant means editing one central, growing method — code that was already written, already tested, already shipped. That editing is exactly the modification OCP wants you to avoid.

Why editing working code is the real cost

Every time you reopen a tested method to add a branch, you risk a typo, a missed case, or a regression in the cases that already worked. You also have to re-test all of them, not just the new one. A long switch on a type is the classic fingerprint of an OCP violation.

The fix: an interface, and one new class per variant

Polymorphism turns the switch inside-out. Instead of one calculator that knows about every shape, you define a small interfaceShape with an area() method — and let each shape implement it. Circle.area() knows the circle formula, Rectangle.area() knows its own, and so on. The calculator no longer asks “what type are you?”; it just calls shape.area() and trusts each shape to answer.

BEFORE · must edit on every new shape AFTER · new shape just plugs in AreaCalculator area(shape): switch (shape.type) { case circle: … case rectangle: … case triangle: … case hexagon: … ← you must add this } ✗ reopen + re-test working code every new shape edits this one class «interface» Shape + area() Circle Rectangle Triangle Hexagon ✓ new class only — nothing else touched old shapes never reopened closed against new shapes? no. it must change for each one.
Before: one AreaCalculator with a switch — every new shape means reopening that class and re-testing it. After: a stable Shape interface with an area() method; Circle, Rectangle, and Triangle each implement it, and a brand-new Hexagon plugs in as its own class (green) without touching a single existing one. The dashed triangle arrows mean “implements the interface.”

Adding a variant becomes purely additive

Now watch what adding a hexagon looks like. You write a new Hexagon class that implements Shape, give it its own area(), and register it. That's it. You did not open AreaCalculator, you did not edit Circle or Rectangle, and the cases that already worked stay byte-for-byte the same. The system grew by addition, not modification — that is OCP working.

This is the Strategy pattern in disguise

The mechanism above — “program to an interface, and swap in a new implementation to get new behaviour” — is exactly the Strategy pattern. Each Shape is a strategy for computing area; each payment method is a strategy for charging; each sort order is a strategy for comparing. Strategy is the everyday tool you reach for to satisfy OCP: it gives you the stable seam (the interface) that new behaviour plugs into. Many design patterns (Strategy, Template Method, Decorator, Observer) exist precisely to make OCP achievable.

“Closed” is never absolute — pick the axis you close against

You can't make code closed against every possible change. The skill is choosing the axis of change you anticipate and closing against that. If new shapes arrive often, put the seam there (a Shape interface). If new shapes never change but the output format does, the seam belongs elsewhere. Close against the change you actually expect — not against everything.

Interactive prototype

See it. Build it. Break it.

A sandboxed, hands-on simulation — no setup, no install. Play with it as you read.

About this simulation

One feature — add a new shape — built two ways, side by side. Click Add Hexagon (or any new shape) and watch what each design forces you to do. In Closed design (switch), the new shape makes you reopen and edit the one central switch: the edited line flashes in the danger colour, a modified existing code ✗ badge appears, and the files you had to touch counter climbs. In Open design (interface), the same shape just drops in as a brand-new class card in green — existing code untouched ✓ — and the touched-files counter only ever shows the one new file. A single fixed note panel narrates each add and the contrast; nothing scrolls or piles up.

Hands-on

Try these yourself

Open the prototype above, predict what happens, then verify.

try 01

Add a shape the painful way

Start in the prototype's Closed design (switch) mode. Click Add Hexagon. Watch the central switch light up: a new case line appears highlighted in the danger colour, a modified existing code ✗ badge shows, and the files you had to touch counter ticks up. You just reopened tested code to add one shape — read the note panel to see why that's the cost OCP avoids.

try 02

Add the same shape the OCP way

Flip to Open design (interface) mode and click Add Hexagon again. This time a brand-new Hexagon class card drops in (green), the badge reads existing code untouched ✓, and the touched-files counter only ever counts the one new file. Same feature, but no existing code was reopened. Compare the two counters side by side.

try 03

Add three more and watch the gap grow

Add a Pentagon, then a Star, then an Ellipse in both modes. In closed mode the switch keeps growing and the touched-files count keeps climbing — every shape reopens the same file. In open mode each shape is just one more independent card and the existing ones are never touched. That widening gap is the value of OCP: change by addition, not by modification.

In practice

When to use it — and what trips people up

When to apply OCP

Reach for OCP at the volatile points of your system — the spots where new variants keep arriving. If you can finish the sentence “we keep adding new kinds of ___” (payment methods, report formats, notification channels, enemy types, discount rules, file parsers), that blank is begging for a stable interface and a class per variant. The tell-tale sign is a switch/if-else on a type that you find yourself editing release after release.

It's also the principle that makes plugin architectures possible: editors, browsers, and games stay closed (you don't recompile the core) yet open (anyone can add a plugin) because the core only talks to a fixed extension interface.

Don't add plug-in seams “just in case” (YAGNI)

OCP has a dark twin: speculative over-abstraction. An interface, a factory, and three classes where one simple function would do is worse code, not better — it's harder to read and you may have guessed the wrong axis of change. Add the seam when the second variant actually shows up (or when you genuinely know it's coming). A switch with two stable cases that never change is fine. Abstract the change you have, not the change you imagine.

What it gives you

  • New behaviour is added by writing a new class — existing, tested code is never reopened, so regressions are far less likely.
  • Each variant lives in its own small, focused class instead of one giant growing method, so the code is easier to read and test in isolation.
  • Enables plugin and extension architectures: the core stays stable while third parties add new behaviour against a fixed interface.
  • Pairs naturally with patterns you already know (Strategy, Template Method, Decorator), giving you a clear, named tool for the job.

Common mistakes

  • Adds indirection — an interface plus several classes is more moving parts than one switch, which can feel heavier for tiny, stable cases.
  • You have to guess the right axis of change up front; close against the wrong axis and the abstraction gets in the way instead of helping.
  • Over-applied, it leads to speculative abstraction (interfaces and factories everywhere) that violates YAGNI and hurts readability.
  • Logic for one operation is now spread across many files, so following a single calculation can mean jumping between classes.

Reference

Code & further reading

A minimal reference implementation and pointers worth bookmarking.

// BEFORE — one calculator with a switch you must EDIT for every new shape.
type Shape = { type: "circle" | "rectangle"; r?: number; w?: number; h?: number };

class AreaCalculator {
  area(s: Shape): number {
    switch (s.type) {                         // grows on every new shape ✗
      case "circle":    return Math.PI * s.r! * s.r!;
      case "rectangle": return s.w! * s.h!;
      // case "triangle": ... ← you must reopen this file to add it
      default: throw new Error("unknown shape");
    }
  }
}

// AFTER — a stable interface; each shape owns its own area().
interface IShape {
  area(): number;
}

class Circle implements IShape {
  constructor(private r: number) {}
  area() { return Math.PI * this.r * this.r; }
}

class Rectangle implements IShape {
  constructor(private w: number, private h: number) {}
  area() { return this.w * this.h; }
}

// The calculator never asks "what type?" — it just trusts the interface.
function totalArea(shapes: IShape[]): number {
  return shapes.reduce((sum, s) => sum + s.area(), 0);  // closed for modification
}

// ADDING A NEW SHAPE = purely additive. No existing class is touched.
class Triangle implements IShape {
  constructor(private base: number, private height: number) {}
  area() { return 0.5 * this.base * this.height; }
}

References & further reading

5 sources

Knowledge check

Did it land?

Quick questions, answers revealed on submit. Sign in to save your best score.

question 01 / 05

What does the Open/Closed Principle actually say?

question 02 / 05

Which piece of code is the classic fingerprint of an OCP violation?

question 03 / 05

How does polymorphism let you satisfy OCP for the shapes example?

question 04 / 05

Which design pattern is the everyday tool for achieving OCP by swapping in new implementations of an interface?

question 05 / 05

Your code already has a switch with two cases that have never changed and aren't expected to. What's the most sensible move?

0/5 answered