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?
answer
- single dispatch = receiver only; arg is static
- double dispatch = accept/visit, two calls
- Visitor: new ops cheap, new types costly
- table keyed by (typeA,typeB): open but unchecked
- collapse to shared attributes → unary dispatch
basics
~20 sOrdinary 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.
solid answer
~50 sMainstream languages give *single* dispatch: `a.hit(b)` selects on `a`'s runtime type, while `b` is resolved statically, so a nested type check reappears. Options: (1) **Double dispatch** — `a.hit(b)` calls `b.hitBy(a as Asteroid)`, so the second call resolves on `b`'s type and the concrete overload is picked by the now-static first type. This is exactly the Visitor pattern's mechanism (`accept`/`visit`). It is fully polymorphic but requires every participant to know every other, giving n² methods and a hierarchy closed to new participants. (2) **A dispatch table** keyed by (typeA, typeB) — data-driven, open to new pairs at runtime, but no compile-time completeness and needs a rule for subtype/symmetry lookup. (3) **Pattern matching on a sealed pair**, giving compile-time exhaustiveness in one readable place but requiring a closed type set. (4) Languages with **multimethods** (CLOS, Julia, Clojure) dispatch on all arguments natively. Choose by whether types or interactions change more often.
code
text · 12 lines// Double dispatch: no type tests, both types concrete at the call body
class Missile : GameObject {
hit(other) = other.hitByMissile(this) // dispatch 1: on `other`
hitByShip(s: Ship) = { s.damage(50) } // dispatch 2 already happened
hitByAsteroid(a) = { this.destroy() }
}
// Trap — overloading is NOT double dispatch:
fun hit(a: Ship, b: Asteroid) { ... }
fun hit(a: Ship, b: GameObject) { ... }
val b: GameObject = Asteroid()
hit(ship, b) // binds to the GameObject overload: chosen from the STATIC typego deeper
Recognise that a normal method call only dispatches on one object and that a nested type check is the naive fallback.
Explain double dispatch concretely (a.hit(b) → b.hitBy(a)) and connect it to Visitor's accept/visit.
Compare double dispatch, type-pair tables, and exhaustive matching by which axis changes, and note the n² method growth and symmetry hazards.
Push toward eliminating the binary method — a canonical intermediate form or attribute-based single dispatch — and weigh open plugin sets versus compile-time exhaustiveness as an architectural commitment.
## 1. The limitation being worked around **Single dispatch** is what almost every mainstream OO language provides: in `receiver.method(arg)`, the runtime uses the *receiver's* actual type to pick the implementation, but `arg` is matched by its **declared** (static) type via overload resolution at compile time. So GRASP Polymorphism, applied naively, only removes the conditional over the first type: ``` class Ship { hit(other: GameObject) { if (other is Asteroid) { ... } // the switch is back else if (other is Missile) { ... } } } ``` The behavior here is genuinely a function of a **pair** of types — a *binary method* problem. Collision physics, currency conversion between two money types, format conversion (source × target), permission checks (principal type × resource type), and mixed-arithmetic (int × complex) are all instances. ## 2. Approach A — Double dispatch (Visitor's mechanism) Make two consecutive polymorphic calls; each resolves one type. ``` interface GameObject { hit(other: GameObject) hitByAsteroid(a: Asteroid) hitByShip(s: Ship) hitByMissile(m: Missile) } class Asteroid implements GameObject { hit(other) = other.hitByAsteroid(this) // 1st dispatch: on `other` hitByShip(s) = { /* ship ↔ asteroid rule */ } // 2nd: overload chosen statically, hitByMissile(m) = { ... } // because `this` is Asteroid here } ``` Call `a.hit(b)`: the first dispatch resolves `a`'s type (choosing which `hitByX` name to send), the second resolves `b`'s type (which class's method runs). Between them, both types are known concretely — with **no type test anywhere**. The **Visitor pattern** is the generalized form: `element.accept(visitor)` dispatches on the element, then calls `visitor.visitCircle(this)`, dispatching on the visitor. Visitor exists precisely to solve the other half of the expression problem: it makes adding new *operations* cheap (write a new visitor) at the price of making new *element types* expensive (every visitor must gain a method). **Costs.** The interface has one method per participant type: n participants → n² method bodies. Adding a participant changes the shared interface and every implementer — impossible if variants live in other teams' modules or plugins. It is verbose, and symmetry (`ship↔asteroid` = `asteroid↔ship`) must be maintained by hand or you get subtly different physics depending on argument order. Encapsulation is also weakened: the second method usually needs the other object's internals. ## 3. Approach B — Dispatch table keyed by the type pair ``` handlers: Map<(Type, Type), (GameObject, GameObject) -> Unit> handlers[(Ship, Asteroid)] = ::shipHitsAsteroid handlers[(Missile, Asteroid)] = ::missileHitsAsteroid resolve(a, b) = handlers[(a.type, b.type)] ?: handlers[(b.type, a.type)]?.flip() ?: defaultCollision ``` All interaction rules sit in one readable registry; new pairs can be registered at runtime by plugins or mods; participants stay ignorant of each other, preserving low coupling. This is how many game engines, rules engines, and conversion frameworks actually do it. **Costs.** No compile-time completeness: a missing pair is a runtime default or crash — mitigate with a startup assertion that the matrix is fully populated. Subtype lookup is subtle: if `Freighter` extends `Ship`, an exact-type map misses it and you must walk the supertype chain, at which point you're implementing your own dispatch semantics (and must define precedence when two rules are equally specific). Reflection-based keys resist static analysis and refactoring tools. ## 4. Approach C — Exhaustive pattern matching on a closed set With sealed/algebraic types and destructuring on a tuple: ``` when (a to b) { is (Ship, Asteroid) -> ... is (Missile, Asteroid) -> ... ... // compiler flags unhandled combinations when a variant is added } ``` All rules visible side by side, compile-time exhaustiveness, participants stay free of each other's knowledge, and new *operations* are cheap. The trade is a **closed** type set: third parties cannot add a participant. Excellent for a fixed domain; wrong for a plugin ecosystem. ## 5. Approach D — Native multiple dispatch CLOS (Common Lisp), Julia, Clojure multimethods, and Groovy's runtime dispatch select on *all* argument types natively. Rules are defined as free-standing methods, so adding either a type or an interaction is an additive change — the expression problem largely dissolves. C#'s `dynamic` and Java's `invokedynamic`-based tricks can emulate it at a runtime-safety cost. Julia's approach shows the flip side: dispatch ambiguity between equally specific methods becomes a real, diagnosable error class, and reasoning about which method runs gets harder as the matrix grows. ## 6. Choosing Ask which axis changes: **participant types** or **interaction rules**? | Situation | Fit | |---|---| | Fixed participants, rules churn | Sealed types + exhaustive match, or Visitor | | Open participant set (plugins/mods) | Dispatch table / registry | | Small n, stability, no external extension | Double dispatch is fine and fully type-safe | | Language supports it | Multimethods | | n large and sparse | Table + explicit default, never n² methods | Also consider **avoiding the problem**: introduce a common intermediate abstraction so the pair collapses to a single dispatch. Money conversion doesn't need currency×currency methods if every currency converts to a canonical base. Collision doesn't need type pairs if objects expose `mass`, `shape`, and `damageProfile` and a single physics routine consumes those. Reducing a binary method to a unary one over shared attributes is usually the *best* answer, and the one most candidates never mention. ## 7. Common trap Overloading is not double dispatch. Declaring `hit(Asteroid)`, `hit(Ship)`, `hit(Missile)` and calling `x.hit(y)` where `y` is declared `GameObject` will always bind to the `GameObject` overload — chosen at compile time from the static type. Many production bugs come from believing otherwise.
- Why is the Visitor pattern usually described as "double dispatch"?`element.accept(visitor)` dispatches on the element's runtime type; inside, the element calls `visitor.visitCircle(this)`, which dispatches on the visitor's runtime type while the element type is now statically known. Two dispatches, both types resolved, no type tests — and it deliberately inverts the expression-problem trade so new operations (visitors) are cheap and new element types are expensive.
- How would you avoid the two-type problem entirely?Collapse the pair into a single dispatch over shared attributes or a canonical intermediate form: convert every currency to a base currency instead of writing currency×currency rules; give game objects `mass`/`shape`/`damage` and run one physics function; convert every input format to a canonical model before rendering to any output format. This turns an n² matrix into 2n adapters.
- What breaks in a (typeA, typeB) dispatch table when subclasses appear?An exact-type key misses subclasses, so `Freighter extends Ship` finds no rule. You must walk supertype chains, define precedence when two rules match with equal specificity, and decide symmetry handling — effectively reimplementing dispatch semantics yourself, which is where ambiguity bugs live.
A hospital where the treatment depends on both the doctor's specialty and the patient's condition. Single dispatch is routing by doctor only — the doctor still has to consult a chart of conditions. Double dispatch is the doctor handing the patient to the sub-specialist who handles exactly that pairing; the pairing chart is what grows quadratically.
saying these in an interview costs you the question
- Believing method overloading dispatches on the runtime type of arguments — it uses the static type, so `x.hit(y)` with `y` declared as the base type always picks the base overload.
- Claiming Visitor makes both new types and new operations cheap; it explicitly buys cheap operations by making new element types expensive.
- Ignoring symmetry, so `a.hit(b)` and `b.hit(a)` produce different results.
- Using a type-pair table with no startup completeness check, so missing combinations surface as production crashes.
- Never considering collapsing the pair into a single dispatch over shared attributes or a canonical form — usually the simplest correct answer.
- Assuming instanceof chains inside a polymorphic method are acceptable "because the outer call is polymorphic".