How to Refactor Code That Violates Open Closed Principle
Code violates the Open/Closed Principle (OCP) when introducing every new business feature, data format, or payment channel requires opening a battle-tested class and appending n...
🟢 Junior Level
Recognizing an OCP Violation in Practice
Code violates the Open/Closed Principle (OCP) when introducing every new business feature, data format, or payment channel requires opening a battle-tested class and appending new if-else or switch branches.
Anti-Pattern: Delivery Calculator Before Refactoring
// ❌ BEFORE: Every new delivery type requires editing this existing method
public class DeliveryCostCalculator {
public BigDecimal calculateCost(String deliveryType, double weightKg, double distanceKm) {
if ("COURIER".equalsIgnoreCase(deliveryType)) {
return BigDecimal.valueOf(weightKg * 10.0 + distanceKm * 5.0 + 150.0);
} else if ("POST".equalsIgnoreCase(deliveryType)) {
return BigDecimal.valueOf(weightKg * 5.0 + distanceKm * 2.0 + 50.0);
} else if ("PICKUP_POINT".equalsIgnoreCase(deliveryType)) {
return BigDecimal.valueOf(distanceKm > 10 ? 100.0 : 0.0);
} else {
throw new IllegalArgumentException("Unsupported delivery type: " + deliveryType);
}
}
}
Why this code is fragile:
- Adding “Drone Express Delivery” requires altering this core class.
- An accidental typo could break calculations for postal delivery (
POST). - It bundles multiple independent business calculation formulas into a single method (violating both SRP and OCP).
Step-by-Step Refactoring: Replace Conditional with Polymorphism
Following Martin Fowler’s refactoring catalog (Replace Conditional with Polymorphism):
Step 1: Define a Strategy Interface
public interface DeliveryStrategy {
boolean supports(String deliveryType);
BigDecimal calculate(double weightKg, double distanceKm);
}
Step 2: Implement Concrete Strategy Classes
public class CourierDeliveryStrategy implements DeliveryStrategy {
@Override
public boolean supports(String deliveryType) {
return "COURIER".equalsIgnoreCase(deliveryType);
}
@Override
public BigDecimal calculate(double weightKg, double distanceKm) {
return BigDecimal.valueOf(weightKg * 10.0 + distanceKm * 5.0 + 150.0);
}
}
public class PostDeliveryStrategy implements DeliveryStrategy {
@Override
public boolean supports(String deliveryType) {
return "POST".equalsIgnoreCase(deliveryType);
}
@Override
public BigDecimal calculate(double weightKg, double distanceKm) {
return BigDecimal.valueOf(weightKg * 5.0 + distanceKm * 2.0 + 50.0);
}
}
Step 3: Delegate via Strategy Registry
public class DeliveryCostCalculator {
private final List<DeliveryStrategy> strategies;
public DeliveryCostCalculator(List<DeliveryStrategy> strategies) {
this.strategies = List.copyOf(strategies);
}
public BigDecimal calculateCost(String deliveryType, double weightKg, double distanceKm) {
return strategies.stream()
.filter(s -> s.supports(deliveryType))
.findFirst()
.orElseThrow(() -> new IllegalArgumentException("Unsupported delivery type: " + deliveryType))
.calculate(weightKg, distanceKm);
}
}
Adding a new delivery mechanism now requires creating a new class; DeliveryCostCalculator is closed for modification.
🟡 Middle Level
1. Refactoring Patterns by Martin Fowler
- Replace Conditional with Polymorphism: Extract repeated
switch/if-elseconditionals checking type codes into polymorphic subclasses or strategy implementations. - Replace Type Code with Strategy / State: When an object alters its internal behavior dynamically based on status (e.g.,
OrderState:NEW,PAID,SHIPPED), delegate operations to a state object. - Strategy Registry / Factory: Decouple strategy execution from strategy selection by building an $O(1)$ lookup registry (
Map<Type, Strategy>).
2. Enterprise Spring Boot Implementation
In enterprise applications, strategies are managed as Spring beans. Injecting a collection into a map provides high performance and automated discovery:
public enum PaymentType {
CARD, PAYPAL, CRYPTO
}
public interface PaymentProcessor {
PaymentType getSupportedType();
PaymentResult process(PaymentRequest request);
}
@Component
public class PayPalPaymentProcessor implements PaymentProcessor {
@Override
public PaymentType getSupportedType() {
return PaymentType.PAYPAL;
}
@Override
public PaymentResult process(PaymentRequest request) {
// PayPal SDK integration
return new PaymentResult(PaymentStatus.SUCCESS, "PP-TX-9901");
}
}
@Service
public class PaymentRoutingService {
private final Map<PaymentType, PaymentProcessor> processorRegistry;
// Spring collects all PaymentProcessor beans into a List and injects them
public PaymentRoutingService(List<PaymentProcessor> processors) {
this.processorRegistry = processors.stream()
.collect(Collectors.toUnmodifiableMap(
PaymentProcessor::getSupportedType,
Function.identity()
));
}
public PaymentResult executePayment(PaymentRequest request) {
PaymentProcessor processor = processorRegistry.get(request.paymentType());
if (processor == null) {
throw new UnsupportedPaymentTypeException(request.paymentType());
}
return processor.process(request);
}
}
3. Safe Refactoring Protocol
To prevent production regressions when converting large conditional blocks:
- Safety Net (Test Harness): Write comprehensive unit tests covering all existing
if-elsebranches before changing production code. - Strangler Method Pattern:
- Extract the interface.
- Implement one strategy for a single branch.
- Route that single branch to the strategy while leaving remaining branches in temporary
elsestatements. - Run tests. Repeat branch by branch.
- Deprecate and Remove: Eliminate legacy conditional logic once all branches are covered by dedicated strategies.
🔴 Senior Level
1. Industrial Refactoring: Branch by Abstraction & Golden Master
In core banking or high-volume payment processing, refactoring a legacy conditional block in a single Pull Request is high-risk.
Branch by Abstraction Workflow:
Step 1: [Clients] ---> [Legacy 25-Branch Conditional Service]
Step 2: [Clients] ---> [Abstraction Router] ---> [Legacy Service]
---> [New Strategy Pipeline]
(Guarded by Feature Toggle)
Step 3: Canary rollout (Route 1% -> 10% -> 100% live traffic to Strategy Pipeline).
Step 4: Decommission legacy service and router abstraction.
Characterization Tests (Golden Master Pattern)
When legacy documentation is outdated and edge cases are undocumented:
- Capture production request/response payloads (10,000+ real transactions).
- Create an automated test comparing the output of the new strategy architecture against the recorded Golden Master baseline.
- Ensure 100% equivalence, including preserving quirky legacy edge-case behaviors that clients rely on.
2. Performance Comparison: Switch vs. Polymorphic Strategy
| Evaluation Metric | switch on int / enum |
Polymorphic Strategy (Interface + Map) |
|---|---|---|
| Bytecode Instructions | tableswitch ($O(1)$ jump table) or lookupswitch ($O(\log N)$ binary search). |
invokeinterface with itable resolution and indirect pointer jumps. |
| JIT Optimization | C2 compiler aggressively inlines branches into contiguous machine code. | Inlining disabled when call site becomes megamorphic ($>2$ implementations). |
| Memory Allocation | Zero heap allocation; values live in CPU registers and thread stack. | Map.get() creates potential CPU cache line misses and boxed object allocations. |
| Architectural Extensibility | Poor: Requires editing source file, recompiling, and redeploying. | Superior: Clean modularity, plugin architectures, and dynamic classloading. |
[!TIP] In ultra-high-throughput systems ($>1,000,000\text{ ops/sec}$, such as network packet parsing or HFT matching engines), keep
switchexpressions on enums for branch performance. In standard enterprise web backends, the sub-microsecond cost of interface dispatch is negligible compared to database and network I/O, making polymorphic strategies the superior choice.
4 Tricky Questions
1. What should you do if legacy if-else branches share and mutate local variables?
Answer: Shared mutable state indicates tight coupling between branches.
- Apply the Extract Parameter Object or Command Pattern: Encapsulate shared execution state inside a dedicated context object (
ExecutionContextorOrderContext). - Pass the context into the strategy interface:
void execute(OrderContext context). - If strategies must execute sequentially and pass computed results forward, replace simple strategy dispatch with a Pipeline / Chain of Responsibility Pattern, where each stage receives an immutable context and returns an enriched copy.
2. Can replacing switch with Map<Type, Strategy> lead to thread-safety bugs or memory leaks?
Answer:
- Thread Safety: If the map is initialized once during startup and published immutably (
Map.copyOforCollections.unmodifiableMap), it is completely thread-safe without synchronization locks. If strategies are added dynamically at runtime, useConcurrentHashMap. - Memory Leaks: A leak can occur if dynamic plugins loaded via custom
ClassLoaderinstances register strategies into a static or root-level application context. The permanent references in the map prevent the pluginClassLoaderfrom being garbage-collected, resulting in a Metaspace Memory Leak.
3. Is refactoring to separate classes always better than an enum with abstract methods?
Answer: No. Java enums support constant-specific class bodies:
public enum Operation {
PLUS { public double apply(double a, double b) { return a + b; } },
MINUS { public double apply(double a, double b) { return a - b; } };
public abstract double apply(double a, double b);
}
If the domain variations are mathematically finite and fixed (e.g., arithmetic operations, chess movements, card suits), an enum with abstract methods is cleaner, faster, and avoids class bloat.
4. How does each strategy manage its own external dependencies without turning the router into a God Object?
Answer: In monolithic code, the central class must inject dependencies for every branch (PayPal client, Stripe client, Crypto RPC client), becoming an overloaded God Object.
With OCP refactoring, each strategy is an independent Spring @Component. It injects only its own required dependencies via constructor injection. The router (PaymentRoutingService) depends solely on the PaymentProcessor interface, keeping fan-out coupling to a minimum.
🎯 Interview Cheat Sheet
Core Concepts:
- Refactoring Pattern: Replace Conditional with Polymorphism (Strategy Pattern + Map Registry).
- Standard Flow: Safety Net Tests $\to$ Extract Interface $\to$ Concrete Strategies $\to$ Strategy Map Lookup $\to$ Delete Legacy Conditionals.
- Spring Boot Pattern: Collect
List<Strategy>in constructor and build an immutableMap<Type, Strategy>. - Zero-Downtime Migration: Use Branch by Abstraction and Feature Toggles to safely migrate production traffic.
- Regression Prevention: Validate refactoring parity against legacy behavior using Characterization Tests (Golden Master).
Red Flags to Avoid:
- ❌ Starting refactoring on a complex conditional block without writing unit tests first.
- ❌ Over-engineering fixed, two-branch conditionals that will never change (violates YAGNI).
- ❌ Passing
Objector usinginstanceofchecks inside strategy classes (violates LSP).