Week 3 Day 1 — Polymorphism and Substitution
Goal
Today I want polymorphism as one API, many implementations, which is the reason Dependency Injection exists.
Main questions:
- What is polymorphism in Java?
- What is the Liskov substitution idea in plain language?
- What is “program to an interface”?
- How does a variable type differ from a runtime type?
- How does Spring use substitution?
1. Polymorphism
Polymorphism means I can treat different objects through the same type and get different behavior.
public interface PaymentGateway {
void charge(Money amount);
}
public class StripeGateway implements PaymentGateway {
public void charge(Money amount) { /* Stripe API */ }
}
public class FakeGateway implements PaymentGateway {
public void charge(Money amount) { /* record in a list for tests */ }
}
PaymentGateway gateway = pickOne(); // Stripe or Fake
gateway.charge(money); // runtime type decides
I wrote the checkout code once against PaymentGateway. Production and tests substitute different objects.
Memory sentence:
Polymorphism: same call, different object, different behavior.
2. Variable type vs runtime type
PaymentGateway gateway = new StripeGateway();
- Variable type (
PaymentGateway): what I can call. The compiler checks this. - Runtime type (
StripeGateway): which method body runs.
Casting to a more specific type is a smell if the rest of the code needs it. If I if (gateway instanceof StripeGateway) inside OrderService, I threw polymorphism away.
3. Substitution (Liskov, without the slogan soup)
If a method asks for PaymentGateway, any implementation should be usable without surprising the caller.
Broken substitution:
public class FreeGateway implements PaymentGateway {
public void charge(Money amount) {
throw new UnsupportedOperationException("cannot charge");
}
}
The type lies. Callers cannot trust charge.
Also broken: an override that ignores preconditions, returns null where the parent promised an object, or throws unexpected unchecked exceptions as a normal path.
Good substitution: FakeGateway records charges. Tests assert on the records. Production StripeGateway talks to Stripe. Both fulfill charge.
Memory sentence:
If the type promises
charge, every implementation must actually charge — or the type is wrong.
4. Program to an interface
public class OrderService {
private final PaymentGateway payments; // type is the abstraction
public OrderService(PaymentGateway payments) {
this.payments = payments;
}
}
OrderService does not mention Stripe. I can add AdyenGateway without editing OrderService.
“Interface” here means abstraction: a Java interface or, less often, an abstract class. The Java keyword is the usual tool because a class can implement several interfaces and because JDK proxies need an interface.
5. Spring connection
Dependency Injection is polymorphism plus a factory:
- I declare the need: constructor parameter of type
PaymentGateway. - I provide implementations as beans.
- The container substitutes one at runtime.
@Service
public class StripeGateway implements PaymentGateway { }
@Service
public class OrderService {
public OrderService(PaymentGateway payments) { ... }
}
If two beans implement PaymentGateway, Spring does not know which to inject. I use @Primary, @Qualifier, or a single @Bean. That is a container problem on top of a type problem. The type problem came first.
Tests:
OrderService service = new OrderService(new FakeGateway());
No Spring. Substitution still works because OrderService programmed to the interface.
6. Common traps
Trap 1: Huge if/else on instanceof instead of a new implementation.
Trap 2: An implementation that throws UnsupportedOperationException for “not needed here.”
The abstraction is too big. Split the interface.
Trap 3: Injecting the concrete StripeGateway everywhere “because there is only one.”
There is only one until the test, the second vendor, or the stub.
Trap 4: Confusing overload with polymorphism.
Overload is compile-time. Polymorphism is runtime dispatch on the instance.
Practice Questions and Answers
Question 1
What is polymorphism?
Answer:
I call a method on a supertype. The JVM runs the implementation from the runtime type. Different objects can share an API and differ in behavior.
Question 2
What does “program to an interface” mean?
Answer:
My class depends on an abstraction (PaymentGateway) rather than a concrete class (StripeGateway). I can substitute fakes in tests and other vendors in production without editing the caller.
Question 3
When is substitution broken?
Answer:
When an implementation cannot fulfill the contract: it throws “not supported,” returns null where an object was promised, or needs the caller to know the concrete type. Then the abstraction is a lie.
Question 4
Why does constructor injection need polymorphism to be useful?
Answer:
If the constructor takes a concrete class I cannot extend or fake cleanly, I am stuck. If it takes an interface, tests and the container can pass any implementation.
Question 5
Two beans implement the same interface. What happens?
Answer:
Spring fails at injection with a no-unique-bean error unless I mark @Primary, use @Qualifier, or inject a List<PaymentGateway>. The type system allowed several substitutes; the container needs a policy.
Memory sentences
Same call, different object, different behavior.
Depend on the API. Let the runtime type provide the body.
DI is substitution: the constructor asks for a type, the container passes an instance.