If you’re familiar with OOP (Object Oriented Programming), you’ll probably recognise the following code: an Animal class, a Dog class, and a method called bark.
It’s good code if you want to learn Java syntax. It’s bad code if you want to understand software architecture. Nobody in the real world writes code to make digital dogs bark. If you only learn these abstract metaphors, you’ll never understand why we use polymorphism or why an interface might be a good idea.
Today, we’ll skip the zoo. We’ll write a real-world e-commerce checkout application using Java and show how using an interface helps your application scale without changing your core code.
The problem: the tightly coupled nightmare
Let’s say you’re building a checkout application. At first, your boss says: “Hey, we only accept Credit Cards.” So you write a service class called CheckoutService that calls a CreditCardAPI.
A month passes. Your boss says: “Hey, we need to accept PayPal.” Then: “Hey, we need to accept Crypto.”
If you’re not using polymorphism, your code will become a nightmare of if/else statements
public class BadCheckoutService {
public void processPayment(String paymentType, double amount) {
if (paymentType.equals("CREDIT_CARD")) {
CreditCardAPI cc = new CreditCardAPI();
cc.charge(amount);
} else if (paymentType.equals("PAYPAL")) {
PayPalAPI pp = new PayPalAPI();
pp.sendFunds(amount);
} else if (paymentType.equals("CRYPTO")) {
CryptoWallet cw = new CryptoWallet();
cw.transfer(amount);
}
}
}
So, why is this bad?
For every new payment type you add, you must go into this fundamental class and change the logic in your if statements. This is a problem because you're altering something that could potentially break your entire checkout process. This is a violation of the Open Closed principle. Our code should be open to extension and closed to modification.
THE SOLUTION: PRACTICAL POLYMORPHISM
We're going to avoid hardcoding every type of payment. What is an Interface? An Interface is a contract that tells the world how something will behave without worrying about how it's implemented.
Step 1: Define the Interface
We're going to make a simple contract that all our payment classes must follow.
public interface PaymentProcessor {
boolean processTransaction(double amount);
}
Step 2: Implement the Interface
Now, we create distinct, isolated classes for each payment method. They all implement PaymentProcessor, meaning they are forced to provide their own specific logic for processTransaction.
public class CreditCardProcessor implements PaymentProcessor {
@Override
public boolean processTransaction(double amount) {
System.out.println("Connecting to Stripe API... Charging £" + amount);
return true;
}
}
public class PayPalProcessor implements PaymentProcessor {
@Override
public boolean processTransaction(double amount) {
System.out.println("Redirecting to PayPal OAuth... Transferring £" + amount);
return true;
}
}
Step 3: The Polymorphic Checkout Service
This is where the magic happens. We inject the interface into our checkout service rather than the concrete classes.
public class CheckoutService {
// The service doesn't care IF it's PayPal or Crypto.
// It only knows it holds an object that can 'processTransaction'.
public void executeCheckout(PaymentProcessor processor, double amount) {
boolean success = processor.processTransaction(amount);
if (success) {
System.out.println("Order complete!");
} else {
System.out.println("Payment failed.");
}
}
}
If your boss wants you to implement Apple Pay tomorrow, you should not touch the existing code in CheckoutService. Instead, create a new class, ApplePayProcessor, that implements PaymentProcessor and pass it in. Your core code remains unchanged and stable.
HND WARNING: AVOID OVER-COMPLICATION
When implementing interfaces in your unit rubrics, ensure that your code remains uncomplicated and easy to maintain. Avoid complicating your code further by implementing advanced Abstract Factory patterns or Spring Boot Dependency Injection (DI) unless your assignment specifically requires you to. It is very easy to get into an error where you create an Interface for every class. For example, you might create an Interface like IUser, an Interface like IDatabase, and an Interface like IButton. This results in too many interfaces. Only create an Interface if you expect that there will be multiple, distinct implementations of that Interface.
Otherwise, use basic constructor passing, like above, if you want top marks.
Top comments (0)