O — Open/Closed Principle
Introduction
Imagine a power strip with six outlets. When you buy a new appliance — a lamp, a phone charger, a monitor — you plug it in. You do not open up the power strip, rewire its internals, and solder in a new outlet. The power strip was designed to accommodate new devices without being modified.
This is exactly what the Open/Closed Principle (OCP) demands of your classes:
Software entities (classes, modules, functions) should be open for extension, but closed for modification.
This principle was originally stated by Bertrand Meyer in 1988 and later adopted by Robert C. Martin as the "O" in SOLID. It addresses one of the most common sources of bugs in software: modifying existing, tested code to add new features.
Every time you open a working class and change its internals to support a new requirement, you risk breaking something that already works. OCP says: design your classes so that you can add new behavior by extending them (writing new code) rather than modifying them (changing old code).
If SRP (the previous tutorial) tells you what each class should be responsible for, OCP tells you how to structure that class so future requirements do not force you to rewrite it. This is why SRP naturally leads to OCP — a class with a single, focused responsibility is much easier to extend without modification.

Why Modifying Existing Code Is Dangerous
You might wonder: "What is so bad about modifying a class? I just add another elif or another case to the switch statement."
The problem is compounding risk. Consider what happens when you modify existing code:
-
Regression risk: Every line you touch in a tested class is a line that might break existing behavior. Automated tests reduce but do not eliminate this risk — tests cannot cover every edge case.
-
Merge conflicts: If multiple features require changes to the same class, developers step on each other's toes. The payment team adds Apple Pay to the same method where the analytics team adds a new tracking event.
-
Testing burden: You must re-run all existing tests for that class, plus write new tests for the new behavior. The blast radius includes everything the modified class touches.
-
Shotgun surgery: When the same type of change ("add a new payment method") requires edits in multiple files — the processor, the validator, the logger, the UI — you have a design problem that OCP can solve.
OCP does not say "never change code." It says: design your code so that the most common types of changes can be accomplished by adding new classes, not by editing old ones.
OCP Violation — The Discount Calculator
Let us look at a concrete violation. A shipping company calculates delivery costs based on the shipping method. Every time a new shipping method is added, the developer must modify the existing class.
# VIOLATION: Adding a new shipping method means modifying this class
class ShippingCostCalculator:
def calculate(self, weight: float, method: str) -> float:
if method == "standard":
return weight * 5.0
elif method == "express":
return weight * 10.0 + 15.0 # base fee + per-kg
elif method == "overnight":
return weight * 20.0 + 30.0
# New method? Add another elif HERE.
# Every addition modifies this tested class.
else:
raise ValueError(f"Unknown shipping method: {method}")
# Every new shipping method forces changes:
# - Modify ShippingCostCalculator (add elif)
# - Re-test ALL existing methods (regression risk)
# - If the class is used in 20 places, all 20 are affected
calculator = ShippingCostCalculator()
print(calculator.calculate(3.0, "standard")) # 15.0
print(calculator.calculate(3.0, "express")) # 45.0
print(calculator.calculate(3.0, "overnight")) # 90.0// VIOLATION: Adding a new shipping method means modifying this class
public class ShippingCostCalculator {
public double calculate(double weight, String method) {
switch (method) {
case "standard":
return weight * 5.0;
case "express":
return weight * 10.0 + 15.0;
case "overnight":
return weight * 20.0 + 30.0;
// New method? Add another case HERE.
default:
throw new IllegalArgumentException(
"Unknown shipping method: " + method
);
}
}
}
// Every new shipping method forces:
// - Modifying ShippingCostCalculator
// - Re-testing ALL existing methods
// - Risk of accidentally breaking existing pricingThis design has a critical flaw: the ShippingCostCalculator class must be modified every time a new shipping method is introduced. If the business adds drone delivery, same-day courier, or freight shipping, the developer must:
- Open
ShippingCostCalculator - Add a new
elif/casebranch - Re-test all existing shipping methods to ensure nothing broke
- Repeat this process in every other class that switches on shipping method (validators, loggers, UI components)
The class is closed for extension (cannot add new methods without modifying it) and open for modification (its internals change with every new feature). This is exactly backwards from what OCP prescribes.
Applying OCP — Strategy-Based Extension
The fix uses polymorphism — the concept we learned in S3. Instead of switching on a string to decide which formula to use, we define an interface (ShippingStrategy) and let each shipping method implement its own calculation. Adding a new method means creating a new class, not modifying the calculator.
# AFTER OCP: New shipping methods require NO changes to existing code
from abc import ABC, abstractmethod
class ShippingStrategy(ABC):
"""Interface for shipping cost calculation.
Each shipping method implements its own formula."""
@abstractmethod
def calculate_cost(self, weight: float) -> float:
pass
@abstractmethod
def get_name(self) -> str:
pass
class StandardShipping(ShippingStrategy):
def calculate_cost(self, weight: float) -> float:
return weight * 5.0
def get_name(self) -> str:
return "Standard"
class ExpressShipping(ShippingStrategy):
def calculate_cost(self, weight: float) -> float:
return weight * 10.0 + 15.0
def get_name(self) -> str:
return "Express"
class OvernightShipping(ShippingStrategy):
def calculate_cost(self, weight: float) -> float:
return weight * 20.0 + 30.0
def get_name(self) -> str:
return "Overnight"
class ShippingCostCalculator:
"""CLOSED for modification — this class never changes.
OPEN for extension — new strategies can be plugged in."""
def calculate(self, weight: float,
strategy: ShippingStrategy) -> float:
return strategy.calculate_cost(weight)
# --- Usage ---
calculator = ShippingCostCalculator()
print(calculator.calculate(3.0, StandardShipping())) # 15.0
print(calculator.calculate(3.0, ExpressShipping())) # 45.0
print(calculator.calculate(3.0, OvernightShipping())) # 90.0
# Adding drone delivery — NO changes to existing code!
class DroneShipping(ShippingStrategy):
def calculate_cost(self, weight: float) -> float:
if weight > 5.0:
raise ValueError("Drone cannot carry over 5kg")
return weight * 25.0 + 10.0 # premium fee
def get_name(self) -> str:
return "Drone"
# Integrates seamlessly:
print(calculator.calculate(2.0, DroneShipping())) # 60.0// AFTER OCP: New shipping methods require NO changes to existing code
interface ShippingStrategy {
double calculateCost(double weight);
String getName();
}
class StandardShipping implements ShippingStrategy {
@Override
public double calculateCost(double weight) {
return weight * 5.0;
}
@Override
public String getName() { return "Standard"; }
}
class ExpressShipping implements ShippingStrategy {
@Override
public double calculateCost(double weight) {
return weight * 10.0 + 15.0;
}
@Override
public String getName() { return "Express"; }
}
class OvernightShipping implements ShippingStrategy {
@Override
public double calculateCost(double weight) {
return weight * 20.0 + 30.0;
}
@Override
public String getName() { return "Overnight"; }
}
// CLOSED for modification — this class never changes.
// OPEN for extension — new strategies can be plugged in.
class ShippingCostCalculator {
public double calculate(double weight, ShippingStrategy strategy) {
return strategy.calculateCost(weight);
}
}
// Adding drone delivery — NO changes to existing code!
class DroneShipping implements ShippingStrategy {
@Override
public double calculateCost(double weight) {
if (weight > 5.0) {
throw new IllegalArgumentException(
"Drone cannot carry over 5kg"
);
}
return weight * 25.0 + 10.0;
}
@Override
public String getName() { return "Drone"; }
}
public class Main {
public static void main(String[] args) {
ShippingCostCalculator calculator = new ShippingCostCalculator();
System.out.println(calculator.calculate(3.0,
new StandardShipping())); // 15.0
System.out.println(calculator.calculate(3.0,
new ExpressShipping())); // 45.0
System.out.println(calculator.calculate(3.0,
new OvernightShipping())); // 90.0
System.out.println(calculator.calculate(2.0,
new DroneShipping())); // 60.0
}
}Look at what changed:
| Aspect | Before (Violation) | After (OCP) |
|---|---|---|
| Adding a new method | Modify ShippingCostCalculator | Create a new class implementing ShippingStrategy |
| Risk to existing code | Every addition risks breaking existing methods | Zero risk — existing code is untouched |
| Testing | Must re-test all methods after any change | Only test the new strategy class |
| Classes modified | 1 (the calculator) | 0 (only new code is added) |
The ShippingCostCalculator is now closed for modification — its code never changes regardless of how many shipping methods are added. It is open for extension — any class implementing ShippingStrategy can be plugged in.
This is the power of polymorphism applied to design. The Strategy Pattern (S10) formalizes this exact approach — you will study it in depth later.
Visualization

The Two Mechanisms for Achieving OCP
There are two main techniques for making a class open for extension but closed for modification:
1. Polymorphism (Meyer's OCP — Most Common)
This is what we demonstrated above: define an interface or abstract class, then extend behavior by creating new implementations. The original class depends on the abstraction, so it never needs to change.
This is the approach you will use most often. It is the foundation of the Strategy Pattern, the Template Method Pattern, the Observer Pattern, and many other design patterns you will learn in S10-S12.
2. Composition and Delegation
Instead of using inheritance to extend behavior, you compose objects. A class accepts collaborator objects through its constructor or methods, and delegates work to them. The collaborators can be swapped out without modifying the original class.
For example, the ShippingCostCalculator we built uses composition — it accepts a ShippingStrategy collaborator and delegates the cost calculation to it. We did not use inheritance to create StandardShippingCostCalculator extends ShippingCostCalculator.
The composition approach is generally preferred because it offers more flexibility — a topic we explored in depth in S4 (Composition vs Inheritance).
Which Mechanism to Choose?
- Use polymorphism when you have a family of algorithms or behaviors that share a common interface (pricing strategies, notification channels, export formats)
- Use composition when you want to combine behaviors dynamically (a decorated logger that adds timestamps AND log levels)
- Often, you will use both together — polymorphic interfaces with composed collaborators
Recognizing OCP Violations
Watch for these code patterns — they are strong signals of OCP violations:
Signal 1: if/elif/switch on Type Strings
Any time you see if type == "A" / elif type == "B" / elif type == "C" with logic that differs per type, you likely have a polymorphism opportunity. The type string should be replaced with a type hierarchy or interface.
Signal 2: Repeated Changes in the Same Location
If your git history shows the same file or method being modified every time a specific kind of feature is added ("add new payment method", "add new export format"), that location violates OCP for that axis of change.
Signal 3: isinstance() or instanceof Checks
Code that checks the concrete type of an object (if isinstance(shape, Circle)) and branches accordingly is doing manually what polymorphism should do automatically.
Signal 4: The Comment "Add new X here"
If your code has comments like // Add new payment methods here or # To add a new export format, add an elif below, the code is explicitly admitting it is not closed for modification.
Common Mistakes
Mistake 1: Trying to Make Everything Open for Every Possible Extension
The trap: Over-engineering a class with extension hooks, abstract methods, and strategy injection points "just in case" future requirements appear.
The reality: OCP is meant to be applied to anticipated axes of change — areas where you know (or strongly suspect) new variants will be added. A shipping company will almost certainly add new delivery methods. But a DateFormatter probably does not need an extension point for "new calendar systems."
Apply OCP where you see a pattern of repeated change. Do not speculatively apply it everywhere — that is a YAGNI violation (S7).
Mistake 2: Confusing OCP with "Never Change Code"
The trap: Refusing to modify any existing class, ever, leading to convoluted workarounds.
The reality: OCP says design for extension in areas where change is likely. Bug fixes, performance improvements, and security patches absolutely require modifying existing code. OCP prevents unnecessary modification for feature additions.
Mistake 3: Using Inheritance When Composition Would Be Better
The trap: Creating deep class hierarchies to achieve OCP: StandardShippingCalculator extends ShippingCalculator, ExpressShippingCalculator extends ShippingCalculator, etc.
The reality: Favor composition (strategy injection) over inheritance for OCP. Inheritance creates tight coupling between parent and child classes, which can violate LSP — the next principle we will study.
OCP and Other SOLID Principles
OCP does not exist in isolation. It connects deeply to the other SOLID principles:
-
SRP enables OCP: When a class has a single responsibility, extending it for that responsibility is straightforward. The monolithic
OrderProcessorfrom our earlier tutorial was impossible to extend without modification because it violated SRP. -
LSP validates OCP: When you extend behavior through subclasses (OCP), LSP ensures those subclasses are actually valid substitutes. If a
DroneShippingstrategy cannot handle packages over 5kg while all other strategies can, does it really satisfy theShippingStrategycontract? This is a LSP concern we will explore next. -
DIP powers OCP: The
ShippingCostCalculatordepends on theShippingStrategyinterface (an abstraction), not onStandardShippingorExpressShipping(concrete details). This dependency on abstraction — DIP — is what makes OCP possible.
You will see OCP applied extensively in the Design Problems course. The Strategy Pattern (S10), which formalizes the OCP approach we used here, appears in Parking Lot (pricing strategies), Elevator System (scheduling algorithms), Car Rental (pricing), Ride-Sharing (driver matching), and many more.
Key Takeaways
- The Open/Closed Principle says classes should be open for extension (add new behavior) but closed for modification (do not change existing tested code)
- if/elif/switch on type is the most common OCP violation — replace it with polymorphism (interfaces and multiple implementations)
- The two mechanisms for OCP are polymorphism (interface-based extension) and composition (delegating to swappable collaborators)
- Apply OCP to anticipated axes of change — areas where new variants are likely to be added. Do not over-engineer extension points for hypothetical scenarios
- OCP is the principle behind the Strategy Pattern (S10), which you will use in nearly every design problem
- In the next tutorial, we will explore the Liskov Substitution Principle — which ensures that the subclasses and implementations you create for OCP actually behave correctly when substituted