skip to content

Polymorphism (GRASP)

When behavior varies by type, let the type decide instead of a switch statement that has to be edited for every new case. This is the concrete move behind the open/closed principle, and reviewers look for it whenever conditionals branch on a kind field.

part ofSoftware design & architectureoverview, primer and where to startread it →
on this pageshow

questions

6

What is the GRASP "Polymorphism" principle in object-oriented design, and what problem does it address?

level: juniorimportance: must knowfreq 62%

answer

  1. type-varying behavior → give it to the types
  2. no switch on type code / kind field
  3. one operation, many implementations
  4. dispatch table replaces the branch
  5. new type easy, new operation hard

basics

~20 s

When behavior differs depending on the kind of thing you have, don't write an if/switch that checks the kind. Give each kind its own version of the operation behind one shared interface, and let the call pick the right version.

solid answer

~50 s

GRASP (General Responsibility Assignment Software Patterns, from Craig Larman) is a catalogue of nine principles for deciding which object should own which responsibility. "Polymorphism" answers the case where behavior varies by type: assign the responsibility to the types themselves, using a polymorphic operation — one operation name declared on a common abstraction, implemented differently by each subtype — instead of a conditional that inspects a type tag or type code. The runtime picks the implementation from the receiver's actual type (dynamic dispatch), so the branching disappears from the caller. The payoff is extensibility: adding a new variant means adding a new implementation, not editing every existing conditional — the Open–Closed shape. The cost is more types, indirection that is harder to read in one place, and pain when a new operation must be added to every existing variant.

code

text · 11 lines
text
// BEFORE — conditional on a type code, duplicated per operation
function area(s):      if s.kind=="CIRCLE" ... elif s.kind=="SQUARE" ...
function perimeter(s): if s.kind=="CIRCLE" ... elif s.kind=="SQUARE" ...

// AFTER — GRASP Polymorphism
interface Shape { area(); perimeter() }
class Circle implements Shape { area()=PI*r*r; perimeter()=2*PI*r }
class Square implements Shape { area()=s*s;    perimeter()=4*s }

// one remaining switch, at the parsing boundary only:
function shapeFrom(json) = registry[json.kind].build(json)

go deeper

for a junior

Name the smell (switch on a type field), name the fix (one interface, one class per variant), and give a two-line shapes or payment example.

for a middle

Add the mechanics — dynamic dispatch, where the remaining factory switch lives, and the cost of extra types and scattered logic.

for a senior

Frame it against the Open–Closed Principle, note that it is the concrete form of GRASP Protected Variations, and raise the expression problem (new types cheap, new operations expensive).

for a principal

Discuss it as an extension-point decision: who owns each variant, whether the variant set is open (plugins) or closed (sealed/exhaustive), and when a data-driven table beats a class hierarchy.

## 1. Where the principle comes from **GRASP** stands for *General Responsibility Assignment Software Patterns* (Craig Larman, *Applying UML and Patterns*). It is not a framework or a library — it is a set of nine naming-and-reasoning tools for the single hardest question in object design: **which object should be responsible for doing this?** The nine are Information Expert, Creator, Controller, Low Coupling, High Cohesion, **Polymorphism**, Pure Fabrication, Indirection, and Protected Variations. ## 2. The problem it names Some behavior is *type-varying*: the correct algorithm depends on what kind of thing you are dealing with. Payroll differs for salaried vs hourly vs contractor. Tax differs by country. Area differs by shape. Export differs by file format. The naive way to express this is a **conditional on type**: an `if/else-if` chain or a `switch` that inspects a **type code** — an enum, a string, a discriminator column, a class check like `instanceof`/`isinstance`/`is` — and then runs the matching block. ``` function area(shape): if shape.kind == "CIRCLE": return PI * shape.r * shape.r if shape.kind == "SQUARE": return shape.side * shape.side if shape.kind == "RECT": return shape.w * shape.h throw "unknown shape" ``` This works, but it has three chronic problems: 1. **Change amplification.** The same switch tends to be copy-pasted for every operation (`area`, `perimeter`, `draw`, `serialize`). Adding one new shape means hunting down and editing *n* switches — classic *shotgun surgery*. 2. **Missed cases.** Nothing forces every switch to handle every code. A forgotten branch silently falls through to a default or throws at runtime. 3. **Knowledge in the wrong place.** The algorithm for a circle lives in a utility function, not with the circle. The data (`r`, `side`, `w`, `h`) has to be exposed publicly so the switch can reach it, which breaks encapsulation — the object becomes a bag of fields. ## 3. The prescription > When related alternatives or behaviors vary *by type*, assign responsibility for the behavior — using **polymorphic operations** — to the types for which the behavior varies. Concretely: declare one operation on a **common abstraction** (an interface, an abstract base class, a protocol, a trait — the exact mechanism varies by language), and implement it once per variant type. ``` interface Shape { area(): Number } class Circle implements Shape { area() = PI * r * r } class Square implements Shape { area() = side * side } class Rectangle implements Shape { area() = w * h } // caller total = sum(shapes.map(s -> s.area())) // no branching at all ``` ## 4. The mechanism that makes it work: dynamic dispatch **Polymorphism** here means *subtype (inclusion) polymorphism*: a variable declared as the abstraction can hold any subtype at runtime. When you call `s.area()`, the runtime looks at the **actual** (dynamic) type of the object, not the declared (static) type, and invokes that type's implementation. Most language runtimes do this with a per-class table of function pointers (a *vtable* / method table) — the call is an indirect jump, typically a handful of nanoseconds. The key insight: **the switch has not disappeared, it has moved into the runtime's dispatch table**, where it is maintained automatically and can never be incomplete. That is the whole trade. Do not confuse this with two other things also called polymorphism: *ad-hoc polymorphism* (overloading — several functions with the same name distinguished by argument types, resolved at compile time) and *parametric polymorphism* (generics/templates — one implementation that works for many types). GRASP Polymorphism is specifically about subtype polymorphism and dynamic dispatch. ## 5. What you gain - **Extension without modification.** A new variant is a new class; existing code is untouched. This is exactly the shape of the **Open–Closed Principle** (open for extension, closed for modification) and is why plugin systems, drivers, and codec registries are all built this way. - **Cohesion.** Everything about a circle lives in `Circle`. The data stays private; the behavior sits next to it (this is GRASP *Information Expert* reinforcing Polymorphism). - **Low coupling for the caller.** The caller depends only on the abstraction and knows nothing about the variant list. - **Total dispatch.** Every subtype necessarily has an implementation, or the code doesn't compile / the class can't be instantiated. ## 6. What you pay - **More types.** Three lines of switch become three files. For two trivial variants this is often a net loss. - **Scattered logic.** You can no longer read all the area formulas side by side; you jump between classes. Reviewers comparing variants for consistency suffer. - **Hard to add operations.** Adding `area` was easy; adding a brand-new operation to a sealed set of variants means editing every class. (Switches have the opposite bias — easy new operations, hard new types. This tension is the *expression problem*.) - **You still need a boundary switch.** Somewhere, a string/enum from JSON, a database column, or a CLI flag must be turned into the right concrete class. That mapping — a factory, registry, or table lookup — is one legitimate remaining conditional, and it should exist in exactly one place. - **Runtime cost.** Indirect calls can inhibit inlining. Real but usually irrelevant; only matters in hot inner loops. ## 7. How to recognise the smell in review Suspect a missing polymorphic design when you see: repeated `switch` on the same field; a `type`/`kind`/`category` string or enum consumed by branching; `instanceof` chains followed by casts; `default: throw new IllegalStateException("unknown type")`; a class with mutually exclusive fields that are only valid for certain `kind` values. ## 8. Related GRASP patterns **Protected Variations** is the general principle (wrap the predicted point of instability in a stable interface); **Polymorphism** is its most common concrete implementation. **Indirection** and **Pure Fabrication** often appear alongside when the variant behavior does not naturally belong to a domain object and you invent a `Strategy`-style helper type instead.

  • If the switch just moves into the runtime's dispatch table, what did you actually gain?
    Completeness and locality. The dispatch table is generated and maintained by the compiler/runtime, so it can never be missing a case or drift out of sync across the codebase, and each variant's logic sits with its own data instead of being smeared across every switch statement that touches the type.
  • Doesn't something still have to decide which concrete class to build?
    Yes. Data arriving from JSON, a database discriminator column, or user input carries a type code, and one factory/registry must map that code to a class. That single mapping is legitimate and expected; the principle is about eliminating the *repeated* switches downstream, not the one at the boundary.
  • How is this different from method overloading?
    Overloading (ad-hoc polymorphism) picks an implementation from the *static* types of arguments at compile time. GRASP Polymorphism relies on subtype polymorphism, where the implementation is chosen at runtime from the receiver's *actual* type — that's what makes it extensible by adding new classes.

A restaurant waiter doesn't carry a checklist saying "if it's pizza, stretch the dough; if it's sushi, slice the fish." The order goes to the station that knows how to make that dish. Adding ramen to the menu means hiring a ramen chef — it does not mean rewriting the waiter.

saying these in an interview costs you the question

  • "Polymorphism removes conditionals from the program" — it relocates one conditional into the runtime dispatch mechanism and keeps a single factory switch at the data boundary.
  • Claiming any use of inheritance is GRASP Polymorphism; the principle is specifically about *type-varying behavior*, not code reuse via a base class.
  • Confusing it with overloading or generics — those are ad-hoc and parametric polymorphism and give no runtime extensibility.
  • Applying it to two trivial cases and shipping five files instead of a four-line switch.
  • Building the class hierarchy but leaving `instanceof` checks in callers, which reintroduces every problem the principle solves.

context

open as a page

A codebase has a `PaymentRecord` with a `method` string field ("CARD", "BANK_TRANSFER", "WALLET"), and the same `switch (method)` appears in fee calculation, settlement-delay estimation, and receipt rendering. Walk through applying the GRASP Polymorphism principle here, and state what you gain and what you give up.

level: middleimportance: must knowfreq 54%

basics

~20 s

Create a PaymentMethod abstraction with fee(), settlementDelay(), and renderReceipt(). Add one class per method implementing all three. Map the stored string to a class once, in a factory. Callers just call the operations — the three switches disappear.

open as a page

GRASP's Polymorphism principle says to replace type-based conditionals with polymorphic operations. In which situations is a plain conditional on type still the better design choice?

level: middleimportance: should knowfreq 38%

basics

~20 s

Keep the conditional when there is only one place that branches, when the variants come from code you can't subclass, when you're at a parsing boundary turning data into objects, or when the set of variants is closed and you want the compiler to check you handled all of them.

open as a page

A polymorphic call selects an implementation from the runtime type of a single receiver object. How do you handle behavior that must vary on the runtime types of TWO objects — for example, collision resolution between `Asteroid`, `Ship`, and `Missile` — and what does each approach cost?

level: seniorimportance: should knowfreq 26%

basics

~20 s

Ordinary method calls pick an implementation from one object's type only. For two, you either chain two polymorphic calls (double dispatch, as in the Visitor pattern), or look the behavior up in a table keyed by the pair of types. Both trade simplicity for extensibility.

open as a page

How does GRASP's Polymorphism principle relate to the Open–Closed Principle, the Liskov Substitution Principle, and GRASP's Protected Variations? Where can a design that uses polymorphism still violate them?

level: seniorimportance: should knowfreq 32%

basics

~20 s

Polymorphism is the usual way to achieve Protected Variations (hide a varying thing behind a stable interface) and to get Open–Closed behavior (add a class, don't edit existing code). Liskov is the condition that makes it safe: every implementation must be usable wherever the interface is expected.

open as a page

At architecture scale, when does replacing type conditionals with a polymorphic subtype hierarchy make a system HARDER to evolve, and what alternatives would you reach for?

level: principalimportance: nice to knowfreq 16%

basics

~20 s

It hurts when the variation isn't really by type, when several variation axes multiply into too many classes, when the variant list is duplicated in databases, APIs and configs, or when adding new operations means changing code owned by other teams. Then prefer data-driven rules, composition, or closed types with exhaustive checks.

open as a page