Object-Oriented Design

Polymorphism

One call, many behaviors: the actual method that runs depends on the object's type at run time.

Level: intermediate

What is it?

Polymorphism lets code call the same operation on different types and get type-specific behavior. Subtype polymorphism dispatches at run time on the object's actual class through a vtable or method lookup. Parametric polymorphism (generics) writes one definition for many types. Ad hoc polymorphism (overloading) chooses among definitions by argument types.

Why does it matter?

It is how you replace long if/elif chains on type with a single call, and how you add a new type without editing the code that uses it. It is the payoff of interfaces and inheritance.

Mental model

A universal remote's 'play' button. You press one button; the DVD player spools a disc, the streaming box opens an app, the radio tunes a station. The remote does not know which device it is talking to — each device knows what 'play' means for it.

How it works

  • Dynamic dispatch: the method called is chosen by the run-time type, usually via a per-class vtable.
  • Static dispatch: chosen at compile time by declared type. Non-virtual methods and overloads work this way.
  • Substitutability: any subtype must work wherever the base type is expected without breaking callers (the Liskov substitution principle).
  • Replace 'if type is X ... elif type is Y' branches with a method on each type.
  • Generics give type-safe reuse without dispatch: List<T> works for every T.

Steps

  • Caller holds a base-typed reference
  • Calls the method by name
  • Runtime finds the object's class
  • Runs that class's version
  • Caller stays unchanged

Code example: One call, different behavior per class

interface Shape { area(): number }

class Circle implements Shape {
  constructor(private r: number) {}
  area() { return Math.PI * this.r ** 2; }
}

class Square implements Shape {
  constructor(private s: number) {}
  area() { return this.s * this.s; }
}

const total = (shapes: Shape[]) => shapes.reduce((sum, s) => sum + s.area(), 0);
console.log(total([new Circle(1), new Square(2)]).toFixed(2));   // 7.14

Real-world usage

  • Plug-in systems and strategy objects
  • GUI frameworks calling draw() on every widget
  • Collections and algorithms that work over any Comparable or Iterable

Common mistakes

  • Using type checks (instanceof) and switches instead of a method on each type.
  • Overriding a method so it violates what callers of the base type expect.
  • Forgetting that some languages need an explicit virtual keyword for dynamic dispatch (C++).

Interview questions

Overloading versus overriding?

Overloading defines several methods with the same name and different parameters, chosen at compile time. Overriding replaces a superclass method in a subclass, chosen at run time by the object's type.

How is dynamic dispatch implemented?

Each class has a table of method pointers (a vtable), and each object holds a pointer to its class's table. A virtual call looks up the method in that table and jumps to it.

What is the Liskov substitution principle?

If S is a subtype of T, then objects of type T can be replaced with objects of type S without breaking the program's correctness. Overriding must honor the base type's contract.

Prerequisites

Related concepts

Continue learning

Browse the full computer science library