Open/Closed Principle (OCP)
SOLID Principles
Imagine you are tasked with creating a service that calculates shipping costs. The costs depend on the shipping method chosen by the user: Standard, Express, or Next-Day. A straightforward initial implementation might look like this:
public class ShippingCalculator {
public double calculateCost(Order order) {
if ("standard".equals(order.getShippingMethod())) {
// Standard shipping calculation
return order.getWeight() * 1.5;
} else if ("express".equals(order.getShippingMethod())) {
// Express shipping calculation
return order.getWeight() * 2.5;
} else if ("next-day".equals(order.getShippingMethod())) {
// Next-day shipping calculation
return order.getWeight() * 3.5;
}
// ... and so on
return 0.0;
}
}
This works for now. But next week, the product team wants to add "International" shipping. The week after, they want to add "Same-Day Priority." Each time, you, the developer, have to go back into the ShippingCalculator
class, add another else if
block, and re-test the entire class.
The core logic of this class is constantly being modified. It is perpetually open for modification, making it fragile and a bottleneck for new feature development.
The Principle: Open for Extension, Closed for Modification
The Open/Closed Principle, originally formulated by Bertrand Meyer, states:
Software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification.
This means you should be able to add new functionality to a system without changing existing, working code.
How is this possible? The key is Abstraction. You design your system so that the stable, core components (which are "closed") depend on abstractions (like interfaces), not on volatile concrete details. New functionality (the "extensions") is then added by creating new concrete classes that implement these interfaces, using the Inheritance.
Think of it like a smartphone. The core phone hardware and its USB-C port are "closed." You can't change them. But the port is "open for extension." You can plug in new devices—chargers, headphones, external drives—without having to take the phone apart and solder new wires. Ironically, companies (Ahem..ple) are trying to take away the ports, but that's a different story.
Pluggable Shipping Strategies
Let's refactor our ShippingCalculator
to be OCP-compliant.
Step 1: Define an Abstraction (The "Port") We create an interface that defines the contract for any shipping strategy.
public interface ShippingStrategy {
double calculate(Order order);
}
Step 2: Create Concrete Extensions (The "Plugins") Now we create a separate class for each shipping method, each implementing the strategy in its own way.
public class StandardShipping implements ShippingStrategy {
@Override
public double calculate(Order order) {
return order.getWeight() * 1.5;
}
}
public class ExpressShipping implements ShippingStrategy {
@Override
public double calculate(Order order) {
return order.getWeight() * 2.5;
}
}
Step 3: The Stable, "Closed" Calculator
Our ShippingCalculator
is now much simpler. It no longer knows or cares about the specific shipping types. It just uses the strategy it's given.
public class ShippingCalculator {
public double calculateCost(Order order, ShippingStrategy strategy) {
// The core logic is closed. It just delegates to the strategy.
return strategy.calculate(order);
}
}
Now, when the product team wants to add "International" shipping, do we modify ShippingCalculator
? No. We simply add a new class:
public class InternationalShipping implements ShippingStrategy {
@Override
public double calculate(Order order) {
// Complex international calculation with tariffs and zones...
return (order.getWeight() * 4.0) + 10.0;
}
}
The existing code is untouched. The system was extended without being modified.
Interview Focus: The Art of Anticipating Change
A key interview insight is knowing that applying OCP everywhere is a form of premature optimization. You don't need to abstract everything from day one.
- When to Apply OCP: Apply it to areas of your system that you can reasonably anticipate will have variations. Common examples include payment methods, notification types, report formats, or validation rules.
- The "Rule of Three" Heuristic: A practical rule of thumb is to wait for the third variation. The first time, you might just write the code. The second time, you might reluctantly add an
if
statement. When the third case is requested, it's a clear signal that this is a point of volatility, and it's time to refactor to a proper OCP-compliant, strategy-based design. Mentioning this demonstrates practical experience, not just academic knowledge.
How OCP Connects is Related to Other Concepts
- Polymorphism: Remember in our Polymorphism chapter, we refactored a
NotificationSender
to eliminate aswitch
statement? OCP is the formal name for the principle we were applying. OCP is the goal we achieve by using polymorphism effectively. - Strategy Design Pattern: The design we ended up with in our above example is a textbook implementation of the Strategy Design Pattern. The goal is OCP, and the Strategy Pattern is a common way to achieve it.
- Pluggable Architecture: OCP is a foundation for building pluggable architectures. It allows you to swap out components without affecting the rest of the system, making it easier to maintain and evolve.