📐 Section 18 · Question #4

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:

  1. Adding “Drone Express Delivery” requires altering this core class.
  2. An accidental typo could break calculations for postal delivery (POST).
  3. 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

  1. Replace Conditional with Polymorphism: Extract repeated switch/if-else conditionals checking type codes into polymorphic subclasses or strategy implementations.
  2. 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.
  3. 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:

  1. Safety Net (Test Harness): Write comprehensive unit tests covering all existing if-else branches before changing production code.
  2. 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 else statements.
    • Run tests. Repeat branch by branch.
  3. 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:

  1. Capture production request/response payloads (10,000+ real transactions).
  2. Create an automated test comparing the output of the new strategy architecture against the recorded Golden Master baseline.
  3. 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 switch expressions 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.

  1. Apply the Extract Parameter Object or Command Pattern: Encapsulate shared execution state inside a dedicated context object (ExecutionContext or OrderContext).
  2. Pass the context into the strategy interface: void execute(OrderContext context).
  3. 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.copyOf or Collections.unmodifiableMap), it is completely thread-safe without synchronization locks. If strategies are added dynamically at runtime, use ConcurrentHashMap.
  • Memory Leaks: A leak can occur if dynamic plugins loaded via custom ClassLoader instances register strategies into a static or root-level application context. The permanent references in the map prevent the plugin ClassLoader from 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 immutable Map<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 Object or using instanceof checks inside strategy classes (violates LSP).