Skip to main content

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:

  1. What is polymorphism in Java?
  2. What is the Liskov substitution idea in plain language?
  3. What is “program to an interface”?
  4. How does a variable type differ from a runtime type?
  5. 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:

  1. I declare the need: constructor parameter of type PaymentGateway.
  2. I provide implementations as beans.
  3. 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.

Next: Week 3 Day 2 — Interfaces vs Abstract Classes