Course content
Open/Closed: Refactor PaymentProcessor
Medium·Tagssolidopen-closedpolymorphismrefactoring
Problem Statement
The starter `PaymentProcessor.process(Payment)` uses an `instanceof` chain to decide how to charge each payment type:
```java
public double process(Payment payment) {
if (payment instanceof CreditCardPayment) {
return payment.getAmount() * 1.025; // 2.5% fee
} else if (payment instanceof DebitCardPayment) {
return payment.getAmount() * 1.01; // 1.0% fee
} else if (payment instanceof UpiPayment) {
return payment.getAmount(); // no fee
} else {
throw new IllegalArgumentException("Unknown payment type");
}
}
```
This violates the Open/Closed Principle. Every time the business adds a new payment type (wallet, BNPL, Apple Pay), this method has to be edited and re-tested. A class that should be stable becomes a magnet for change.
Refactor the design so that adding a new payment type requires zero changes to `PaymentProcessor`. After your refactor, the design must expose this exact public API:
```java
abstract class Payment {
public Payment(double amount);
public double getAmount();
public abstract double getTotalCharge(); // each subtype implements its own fee rule
}
class CreditCardPayment extends Payment { ... } // 2.5% fee on top of amount
class DebitCardPayment extends Payment { ... } // 1.0% fee on top of amount
class UpiPayment extends Payment { ... } // no fee, charge equals amount
class PaymentProcessor {
public double process(Payment payment);
// delegates to payment.getTotalCharge(); contains no type checks.
}
```
The `getTotalCharge()` method is already abstract in the starter, but each subclass returns 0. Move the fee logic from `PaymentProcessor.process` into each subclass, and replace the body of `process` with a single call to `payment.getTotalCharge()`.
The validator runs five checks:
1. `CreditCardPayment(100).getTotalCharge()` returns 102.5 when called directly on the subclass. The fee logic must live in the subclass, not in the processor.
2. `DebitCardPayment(100).getTotalCharge()` returns 101.0.
3. `UpiPayment(100).getTotalCharge()` returns 100.0.
4. The validator defines its own subclass of `Payment` that the user has never seen, with a custom fee rule. It then calls `processor.process(...)` on an instance of that subclass and verifies the correct charge comes back. If the user's `process` method still uses `instanceof`, this check fails because the chain does not recognize the new subclass.
5. `processor.process(CreditCardPayment(100))` returns 102.5 end-to-end, confirming that the processor delegates correctly to the subclass logic for existing types as well.
The smelly starter passes only check 5 out of the box. The other four require the actual refactor.
Examples
Example 1
Input
new CreditCardPayment(200).getTotalCharge()Output
"205.0"Why
The credit card fee is 2.5%. The fee logic lives in the subclass, not in the processor.
Example 2
Input
new PaymentProcessor().process(new UpiPayment(500))Output
"500.0"Why
UPI has no fee. The processor just delegates to UpiPayment.getTotalCharge().
Example 3
Input
new PaymentProcessor().process(new GiftCardPayment(100))Output
"depends on GiftCardPayment.getTotalCharge()"Why
Adding a new payment type is a new class. The processor handles it without any change because it only depends on the Payment abstraction.
Constraints
- •Refactor must keep the four classes named: Payment, CreditCardPayment, DebitCardPayment, UpiPayment, PaymentProcessor.
- •Payment must remain an abstract class with an abstract getTotalCharge() method.
- •PaymentProcessor.process must accept a Payment parameter (not a narrower type).
- •PaymentProcessor.process must not branch on the runtime type of its argument.
Hints
Stuck? Reveal a nudge toward the right pattern, one step at a time.
Hint 1
The fee rule for each payment type belongs inside that subclass's getTotalCharge method, not inside PaymentProcessor. Move CreditCardPayment's 2.5% fee into CreditCardPayment.getTotalCharge, the 1.0% fee into DebitCardPayment.getTotalCharge, and so on.
Hint 2
Once each subclass's getTotalCharge returns the correct value, PaymentProcessor.process becomes one line: return payment.getTotalCharge();.
Hint 3
Do not branch on the runtime type inside process. The whole point of the refactor is that process should not know which concrete subclass it received.
Hint 4
Java's polymorphic dispatch handles the type lookup for you. When you call payment.getTotalCharge(), the JVM picks the right subclass implementation based on the actual object, with no help from your code.
Hint 5
If you keep the instanceof chain and just add new branches, you are not applying the principle. The validator defines its own subclass that you cannot anticipate, so any chain that lists known types will fall through and fail.