All Posts

How the Open/Closed Principle Enhances Software Development

Software entities should be open for extension, but closed for modification. We explain the 'O' i...

Abstract AlgorithmsAbstract Algorithms
ยทยท4 min read
Share
Share on X / Twitter
Share on LinkedIn
Copy link

TLDR: The Open/Closed Principle (OCP) states software entities should be open for extension (add new behavior) but closed for modification (don't touch existing, tested code). This prevents new features from introducing bugs in old features.


๐Ÿ“– "Open for Extension, Closed for Modification" โ€” What That Actually Means

Every time you open a file to add a new feature, you risk breaking something that already works.

OCP says: instead of modifying existing code, design it so you can add new behavior by adding new code โ€” not changing old code.

The classic metaphor: a plugin system. You don't modify Firefox to add an ad-blocker. You write a new plugin that extends Firefox. The browser itself never changed.


๐Ÿ” The Fragile Base Class Problem

Here's a payment processor that violates OCP:

class PaymentProcessor:
    def process(self, payment_type, amount):
        if payment_type == "credit_card":
            print(f"Processing credit card: ${amount}")
        elif payment_type == "paypal":
            print(f"Processing PayPal: ${amount}")
        # Every new payment method = edit THIS file

Each time you add a new payment method (Apple Pay, Crypto), you edit PaymentProcessor.process(). You risk:

  • Breaking credit card handling.
  • Merging conflicts from other teams editing the same file.
  • Growing the if/elif chain indefinitely.

โš™๏ธ OCP via Polymorphism: Extending Without Modifying

The fix: define a common abstraction and extend it.

from abc import ABC, abstractmethod

class PaymentMethod(ABC):
    @abstractmethod
    def process(self, amount: float) -> None:
        ...

class CreditCard(PaymentMethod):
    def process(self, amount):
        print(f"Processing credit card: ${amount}")

class PayPal(PaymentMethod):
    def process(self, amount):
        print(f"Processing PayPal: ${amount}")

class CryptoWallet(PaymentMethod):           # NEW โ€” no existing file touched
    def process(self, amount):
        print(f"Processing crypto: ${amount}")

class PaymentProcessor:
    def process(self, method: PaymentMethod, amount: float):
        method.process(amount)               # Never changes
classDiagram
    class PaymentMethod {
        <>
        +process(amount)
    }
    PaymentMethod <|-- CreditCard
    PaymentMethod <|-- PayPal
    PaymentMethod <|-- CryptoWallet
    PaymentProcessor --> PaymentMethod

PaymentProcessor is now closed for modification. You can add 50 new payment methods and never touch it.


๐Ÿง  Java Interface Implementation

In Java, the same pattern uses interfaces:

public interface PaymentMethod {
    void process(double amount);
}

public class CreditCard implements PaymentMethod {
    public void process(double amount) {
        System.out.printf("Credit card: $%.2f%n", amount);
    }
}

public class CryptoWallet implements PaymentMethod {   // New โ€” zero changes to CreditCard
    public void process(double amount) {
        System.out.printf("Crypto: $%.2f%n", amount);
    }
}

public class PaymentProcessor {
    public void charge(PaymentMethod method, double amount) {
        method.process(amount);   // Closed โ€” never modified
    }
}

Testing benefit: You can write a FakePayment implementing PaymentMethod for unit tests without any production code changes.


โš–๏ธ When OCP Is Over-Engineering

OCP adds indirection. That's only worth the cost when variation is genuinely expected.

ScenarioApply OCPSkip OCP
Multiple implementations of the same behavior (e.g., payment types)โœ…โ€”
Behavior that varies by external configuration or runtimeโœ…โ€”
Single implementation that will never varyโ€”โœ…
Early-stage prototyping where requirements are unclearโ€”โœ…
One-off scripts or CLI toolsโ€”โœ…

The trap: abstracting too early. If you build an interface for a class that only ever has one implementation, you've added complexity with no benefit. OCP is reactive โ€” apply it when the second variant appears, or when you are confident variation is coming.


๐Ÿ“Œ Summary

  • Closed for modification: Once a class is tested and deployed, resist changes that could introduce regressions.
  • Open for extension: Use abstract classes or interfaces to define the contract; add new types as new classes.
  • Polymorphism is the mechanism: A PaymentProcessor that takes any PaymentMethod never needs to know about new payment types.
  • Don't over-abstract: OCP pays off when variation is real. One class with one job needs no abstraction layer.

๐Ÿ“ Practice Quiz

  1. A developer adds a new elif for every new report format to a ReportGenerator class. Which principle is being violated?

    • A) Single Responsibility Principle.
    • B) Open/Closed Principle โ€” the class requires modification for extension.
    • C) Liskov Substitution Principle.
      Answer: B
  2. What is the primary mechanism for achieving OCP in object-oriented code?

    • A) Static methods.
    • B) Polymorphism via abstract classes or interfaces.
    • C) Inheritance without abstraction.
      Answer: B
  3. You have a class that formats invoices. Currently it only formats PDFs and never will do anything else. Should you apply OCP?

    • A) Yes, always add an abstraction layer.
    • B) No โ€” single implementation, no anticipated variation. OCP adds cost without benefit.
    • C) Yes, but only in Java, not Python.
      Answer: B

Abstract Algorithms

Written by

Abstract Algorithms

@abstractalgorithms