OOP Concepts · Design Principles

Open/Closed Principle

The O in SOLID: you should be able to add new behaviour without editing code that already works. New feature → new class, not another else if.

Intermediate 5 min Lesson 24 of 38 Runnable examplesExamples use modern JavaScript (ES2022 private fields) and C# 13.
Before you start:SOLID Principles
0/38 in OOP Concepts
On this page · 5 min · Intermediate

In simple words#

A phone accepts new apps without opening its hardware. Code should accept new behaviour (open for extension) without editing tested code (closed for modification).

Open
Add new behaviour by adding code.
Closed
Don’t edit existing, working code.
How
Interfaces, strategy classes, plug-ins.

Learning objectives#

  • State the Open/Closed Principle
  • Recognise the “growing if/else” smell
  • Extend behaviour with polymorphism
  • Know when OCP is worth applying

Explanation#

Open for extension: you can add new behaviour. Closed for modification: you do not have to change existing, tested code to do it.

The classic smell is a function with a long if/else or switch on a type: if (type === "festival") ... else if (type === "loyalty") .... Every new type means editing it, retesting it and risking a bug in the old branches.

The fix: define an abstraction (Discount with apply(amount)) and put each rule in its own class. The checkout loops over discounts and never changes again.

Adding a StudentDiscount next month is a new file; nothing old is touched.

Apply OCP where change is likely — pricing rules, payment methods, export formats. Do not build plug-in systems for things that never change.

Syntax#

C# Illustrative
public interface IDiscount { decimal Apply(decimal amount); }


public class FestivalDiscount : IDiscount { public decimal Apply(decimal a) => a * 0.8m; }
public class LoyaltyDiscount  : IDiscount { public decimal Apply(decimal a) => a - 100; }


public class Checkout(IEnumerable<IDiscount> discounts)
{
    public decimal Total(decimal amount) => discounts.Aggregate(amount, (a, d) => d.Apply(a));
}

Code example: Discount rules you can add without editing#

JavaScriptDiscount rules you can add without editing Runnable
// ✗ Before: every new rule means editing this function
// function discount(type, amount) {
//   if (type === 'festival') return amount * 0.8;
//   else if (type === 'loyalty') return amount - 100;
//   ...
// }


// ✓ After: closed for modification...
class Checkout {
  constructor(discounts) { this.discounts = discounts; }
  total(amount) {
    return this.discounts.reduce((current, d) => d.apply(current), amount);
  }
}


// ...open for extension: each rule is its own class
class FestivalDiscount { apply(amount) { return amount * 0.8; } }
class LoyaltyDiscount { apply(amount) { return amount - 100; } }


console.log(new Checkout([new FestivalDiscount()]).total(1000));
console.log(new Checkout([new FestivalDiscount(), new LoyaltyDiscount()]).total(1000));


// Next month: a new rule. Nothing above changes.
class StudentDiscount { apply(amount) { return amount * 0.9; } }
console.log(new Checkout([new FestivalDiscount(), new LoyaltyDiscount(), new StudentDiscount()]).total(1000));
Expected output
800
700
630
Opens this example in the full playground

Step-by-step explanation#

  1. Checkout only knows that each discount has apply(amount). It never checks discount types.
  2. reduce passes the amount through every discount in order: 1000 → 800 → 700.
  3. StudentDiscount is added as a brand-new class; Checkout, FestivalDiscount and LoyaltyDiscount are untouched.
  4. The old, tested code cannot break because it was not edited.

Real-world use case#

ASP.NET Core middleware follows OCP: you extend the request pipeline with app.UseXyz() without modifying the framework. Your own export feature can register IExporter implementations (PDF, CSV, Excel) the same way.

Common mistakes#

Switching on type names

switch (shape.GetType().Name) must be edited for each new shape. Use polymorphism instead.

Premature abstraction

Creating interfaces for rules that will never change adds complexity without benefit. Apply OCP where change is expected.

Best practices#

  • Hide varying behaviour behind an interface
  • Register new implementations instead of editing switch statements
  • Wait for the second or third variation before abstracting
  • Combine with the Strategy pattern

Remember this#

Open for extension, closed for modification.

New rule = new class, not new else-if.

Abstract where change is likely, not everywhere.

Practice exercise#

Your task: Make shipping cost extensible: create StandardShipping (₹50) and ExpressShipping (₹150), each with cost(). Write a function orderTotal(amount, shipping) that never needs editing when a new shipping type appears. Then add FreeShipping.

Need a hint?
orderTotal returns amount + shipping.cost().
Show solution
JavaScript Runnable
class StandardShipping { cost() { return 50; } }
class ExpressShipping { cost() { return 150; } }
const orderTotal = (amount, shipping) => amount + shipping.cost();


console.log(orderTotal(500, new StandardShipping())); // 550
console.log(orderTotal(500, new ExpressShipping()));  // 650


class FreeShipping { cost() { return 0; } }            // added later
console.log(orderTotal(500, new FreeShipping()));     // 500
Opens this example in the full playground

Mini quiz#

Open/Closed Principle quizMultiple choice Question 1 of 3

What does “closed for modification” mean?

Score so far: 0/0

Want more? Practise all OOP Concepts MCQs · OOP Concepts interview questions

My notes#

Saved in this browser.

Progress is saved in this browser. Sign in to sync.