LLD
Machine Coding
Interview Course
Java โ€ข Interview Prep
๐Ÿ“ SOLID Principles/

Open/Closed Principle (OCP)

Lesson 2 of 5

Open/Closed Principle (OCP)


What is it? (The Analogy)

Think about a game console like a PlayStation or Nintendo Switch. When the console was built, it could play the games available at launch. But here's the brilliant part -- you can add new games (cartridges, downloads) without opening up the console and rewiring its circuit board. The console is open for extension (new games) but closed for modification (you never touch the hardware).

Now imagine a terrible console where every time a new game came out, you had to physically open the console, solder new chips, and rewire components. Every new game risks frying the whole system. That's what code without OCP looks like -- every new feature means cracking open existing, working code and risking breakage.

The Open/Closed Principle says: Software entities (classes, modules, functions) should be open for extension but closed for modification. You should be able to add new behavior WITHOUT changing old, tested, working code.


Why do we need it?

Let's feel the pain. Imagine you're building a drawing app that calculates the area of shapes:

java
1// BAD EXAMPLE -- Violates OCP!
2public class AreaCalculator {
3
4    public double calculateArea(Object shape) {
5        if (shape instanceof Circle) {
6            Circle circle = (Circle) shape;
7            return Math.PI * circle.getRadius() * circle.getRadius();
8        }
9        else if (shape instanceof Rectangle) {
10            Rectangle rect = (Rectangle) shape;
11            return rect.getWidth() * rect.getHeight();
12        }
13        else if (shape instanceof Triangle) {
14            Triangle tri = (Triangle) shape;
15            return 0.5 * tri.getBase() * tri.getHeight();
16        }
17        // PROBLEM: What happens when we add Pentagon? Hexagon? Star?
18        // We have to MODIFY this method every single time!
19        // Every modification risks breaking Circle, Rectangle, or Triangle.
20        throw new IllegalArgumentException("Unknown shape!");
21    }
22}

Every time a new shape is added, you modify the calculateArea method. That method is getting longer, more fragile, and harder to test. You're touching code that already works perfectly fine for circles and rectangles.

The pain: The if-else chain grows forever. Every new feature modifies existing code. Every modification can introduce bugs in things that were already working. This is called "shotgun surgery" -- one change blasts holes everywhere.


How it works -- Step by Step

  1. Identify what varies -- Ask yourself: "What new types or behaviors will be added in the future?" That's the part that should be extensible.
  1. Define an abstraction -- Create an interface or abstract class that represents the common behavior (the "contract" every new type must follow).
  1. Move specific logic into implementations -- Each variant (circle, rectangle, etc.) implements the abstraction with its own logic.
  1. Depend on the abstraction, not the specifics -- Your main code works with the interface. It never needs to know about specific implementations.
  1. Add new behavior by adding new classes -- New features = new classes that implement the interface. Old code stays untouched.

Let's Build It Together

Let's build a Zoo Ticket Pricing System using OCP. We'll start with the bad way, then refactor.

java
1// ========================================
2// Step 1: Define the abstraction (the contract)
3// ========================================
4
5// Every ticket type MUST be able to calculate its own price
6public interface TicketPricing {
7    double calculatePrice(int basePrice);
8    String getTicketType();
9}
java
1// ========================================
2// Step 2: Implement specific ticket types
3// Each one knows its own pricing rules
4// ========================================
5
6public class ChildTicket implements TicketPricing {
7    @Override
8    public double calculatePrice(int basePrice) {
9        // Kids get 50% off!
10        return basePrice * 0.5;
11    }
12
13    @Override
14    public String getTicketType() {
15        return "Child (under 12)";
16    }
17}
18
19public class AdultTicket implements TicketPricing {
20    @Override
21    public double calculatePrice(int basePrice) {
22        // Adults pay full price
23        return basePrice;
24    }
25
26    @Override
27    public String getTicketType() {
28        return "Adult";
29    }
30}
31
32public class SeniorTicket implements TicketPricing {
33    @Override
34    public double calculatePrice(int basePrice) {
35        // Seniors get 30% off
36        return basePrice * 0.7;
37    }
38
39    @Override
40    public String getTicketType() {
41        return "Senior (65+)";
42    }
43}
java
1// ========================================
2// Step 3: The ticket counter uses the ABSTRACTION
3// It NEVER needs to be modified for new ticket types!
4// ========================================
5
6public class ZooTicketCounter {
7    private int basePrice;
8
9    public ZooTicketCounter(int basePrice) {
10        this.basePrice = basePrice;
11    }
12
13    // This method works for ANY ticket type -- now AND in the future
14    public void sellTicket(TicketPricing ticket) {
15        double price = ticket.calculatePrice(basePrice);
16        System.out.println("Sold: " + ticket.getTicketType()
17                         + " ticket for $" + String.format("%.2f", price));
18    }
19
20    // Process a batch of tickets -- also works with any type!
21    public double calculateBatchTotal(List<TicketPricing> tickets) {
22        double total = 0;
23        for (TicketPricing ticket : tickets) {
24            total += ticket.calculatePrice(basePrice);
25        }
26        return total;
27    }
28}
java
1// ========================================
2// Step 4: Now let's see the magic -- adding a NEW ticket type
3// WITHOUT changing ANY existing code!
4// ========================================
5
6// New requirement: Student discount! Just add a new class.
7public class StudentTicket implements TicketPricing {
8    @Override
9    public double calculatePrice(int basePrice) {
10        // Students get 25% off
11        return basePrice * 0.75;
12    }
13
14    @Override
15    public String getTicketType() {
16        return "Student (with valid ID)";
17    }
18}
19
20// Another new requirement: VIP tickets with backstage tour!
21public class VIPTicket implements TicketPricing {
22    @Override
23    public double calculatePrice(int basePrice) {
24        // VIP costs 3x base and includes backstage tour
25        return basePrice * 3.0;
26    }
27
28    @Override
29    public String getTicketType() {
30        return "VIP (includes backstage tour)";
31    }
32}
java
1// ========================================
2// Step 5: Run it all!
3// ========================================
4
5public class ZooApp {
6    public static void main(String[] args) {
7        ZooTicketCounter counter = new ZooTicketCounter(20); // $20 base price
8
9        // Sell different types of tickets
10        counter.sellTicket(new ChildTicket());    // $10.00
11        counter.sellTicket(new AdultTicket());    // $20.00
12        counter.sellTicket(new SeniorTicket());   // $14.00
13        counter.sellTicket(new StudentTicket());  // $15.00 -- NEW! No old code changed!
14        counter.sellTicket(new VIPTicket());      // $60.00 -- NEW! No old code changed!
15
16        // Calculate a family trip total
17        List<TicketPricing> familyTrip = List.of(
18            new AdultTicket(),
19            new AdultTicket(),
20            new ChildTicket(),
21            new SeniorTicket()
22        );
23        double total = counter.calculateBatchTotal(familyTrip);
24        System.out.println("\nFamily trip total: $" + String.format("%.2f", total));
25    }
26}

Aha moment: We added StudentTicket and VIPTicket without touching ZooTicketCounter, ChildTicket, AdultTicket, or SeniorTicket. The old code is closed for modification but the system is open for extension. That's OCP in action!


Visual Mental Model

java
1                    +-------------------+
2                    |  TicketPricing    |  <-- The abstraction (interface)
3                    |  (interface)      |
4                    |                   |
5                    | + calculatePrice()|
6                    | + getTicketType() |
7                    +-------------------+
8                           /|\
9            ________________|__________________
10           |        |        |        |        |
11           v        v        v        v        v
12       +-------+ +------+ +------+ +-------+ +-----+
13       | Child | |Adult | |Senior| |Student| | VIP |
14       |Ticket | |Ticket| |Ticket| |Ticket | |Ticket|
15       +-------+ +------+ +------+ +-------+ +-----+
16
17  ZooTicketCounter only depends on TicketPricing (the interface).
18  New ticket types plug in WITHOUT modifying the counter.
19
20  CLOSED for modification: ZooTicketCounter never changes.
21  OPEN for extension: Just implement TicketPricing.

Common Mistakes & Gotchas

  • Not every if-else is bad -- OCP applies when new types are added frequently. If you have an if-else that checks for "valid" vs "invalid" and that's it, don't over-engineer it.
  • Premature abstraction -- Don't create interfaces for things that will never have more than one implementation. Apply OCP when you see or expect variation.
  • Forgetting that OCP builds on SRP -- If your class has multiple responsibilities, it's hard to make it open/closed. Clean up SRP first, then apply OCP.
  • Using inheritance when composition works better -- OCP doesn't mean "always use inheritance." Interfaces and composition are often cleaner than deep class hierarchies.
  • **The instanceof smell** -- If you see a chain of instanceof checks, it's almost always a sign that OCP is being violated. Each type should know its own behavior.

Interview Tip

When interviewers give you a system to design, actively look for parts that will grow over time. Payment methods, notification types, discount rules, file formats -- these are all places where OCP shines. Say something like: "I'll define a PaymentMethod interface here so we can add new payment types in the future without modifying the checkout logic." This shows you think about extensibility and maintainability, not just getting it to work.


Quick Quiz

  1. You have a NotificationSender class with methods sendEmail(), sendSMS(), and sendPush(). Your boss wants to add Slack and WhatsApp notifications. How would you redesign this using OCP?
  1. Why is the Open/Closed Principle often described as depending on SRP being in place first?
  1. Can you think of a real-world object (not code) that is "open for extension but closed for modification"? (Hint: think about things with plugins, accessories, or add-ons.)

Summary -- Key Takeaways

  • Open for extension, closed for modification means you add new behavior by writing new code, not by changing existing working code.
  • Use interfaces and abstractions to define the "shape" of behavior. Specific implementations live in their own classes.
  • **The instanceof / if-else chain is a code smell** -- it usually means the class needs to be refactored using OCP.
  • OCP reduces risk -- when you never modify tested code, you can't break what already works. New features are isolated in new classes.