skip to content

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%

answer

  1. PV = why, Polymorphism = how, OCP = result, LSP = precondition
  2. UnsupportedOperation ⇒ strengthened precondition ⇒ LSP break
  3. instanceof in caller ⇒ abstraction missing an operation
  4. contract test = executable LSP
  5. wrong variation axis = most expensive failure

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.

solid answer

~50 s

They stack. **Protected Variations** (GRASP) is the goal: identify a predicted point of instability and wrap it in a stable interface. **Polymorphism** (GRASP) is its dominant implementation: the interface is a supertype, the variations are subtypes. **Open–Closed** (SOLID) is the resulting property: new variants extend behavior without modifying existing code. **Liskov Substitution** (SOLID) is the correctness precondition: subtypes must honor the supertype's contract — no strengthened preconditions, no weakened postconditions, preserved invariants, no surprise exceptions — otherwise callers must type-check, which destroys all three. Violations persist despite polymorphism when: subclasses throw `UnsupportedOperation` for interface methods; callers reintroduce `instanceof` and downcasts; the interface leaks variant-specific concepts (`isCreditCard()`, `getWalletId()`); a new variant forces changes in a shared base class or factory chain; or the abstraction was drawn along the wrong variation axis, so real change still cuts across every implementation.

code

text · 15 lines
text
// LSP violation that silently destroys OCP
interface PaymentMethod { charge(a); refund(a) }

class PrepaidVoucher : PaymentMethod {
    refund(a) = throw UnsupportedOperation()   // strengthened precondition
}

// callers are forced back into type tests -> no longer open for extension
if (m is PrepaidVoucher) showNoRefundNotice() else m.refund(a)

// FIX A — segregate the capability
interface Refundable { refund(a) }             // only refundable methods implement it

// FIX B — make it part of the contract, so the branch is legitimate
interface PaymentMethod { charge(a); supportsRefund(): Boolean; refund(a) }

go deeper

for a junior

State the chain: interface hides the varying part, subtypes vary, new variants don't require editing old code, and every subtype must work wherever the interface is expected.

for a middle

Add the LSP failure mode — UnsupportedOperation and downcasting callers — and the Interface Segregation repair.

for a senior

Name PV as the goal and Polymorphism as one mechanism among several, list concrete OCP failures (parallel hierarchies, leaky interface, external enumerations) and enforce LSP with contract tests.

for a principal

Focus on choosing the variation axis from evidence, the cost of a wrong seam, resisting speculative abstraction via YAGNI, and how the enumeration escaping into schemas, APIs, and configs turns 'add one class' into a cross-system change.

## 1. The four ideas, defined - **GRASP Protected Variations (PV).** "Identify points of predicted variation or instability; assign responsibilities to create a stable interface around them." It is the *why* — a risk-management principle about where change is expected. - **GRASP Polymorphism.** "When behavior varies by type, assign the responsibility to the types using polymorphic operations." It is the *how* — the most common mechanism for PV. (Other PV mechanisms exist: data-driven configuration, service lookup, interpreters, reflection.) - **Open–Closed Principle (OCP, Meyer/Martin, the O in SOLID).** "Software entities should be open for extension but closed for modification" — you can add behavior without editing existing, tested code. It is the *property* a good polymorphic design exhibits. - **Liskov Substitution Principle (LSP, the L in SOLID).** "If S is a subtype of T, objects of type T may be replaced with objects of type S without altering the correctness of the program." It is the *precondition* that makes the whole stack sound. A compact way to say it: **PV says wrap the variation. Polymorphism says wrap it with a type hierarchy. OCP is what you get if you did it right. LSP is what you must not break while doing it.** ## 2. Why LSP is load-bearing Suppose `PaymentMethod.refund(amount)` exists, and `PrepaidVoucher.refund()` throws `UnsupportedOperationException`. Formally this **strengthens a precondition** ("only call me if the method is refundable"), which LSP forbids. Practically, callers respond the only way they can: ``` if (method is PrepaidVoucher) { showNoRefundNotice() } else { method.refund(amount) } ``` The type check is back. Extension is no longer free — every new non-refundable variant means editing callers — so OCP is gone, and the interface no longer protects anything, so PV is gone. **One LSP violation collapses all three.** The substitutability rules worth naming precisely: - **Preconditions may not be strengthened** — a subtype cannot demand more of callers (narrower input ranges, extra setup, additional non-null requirements). - **Postconditions may not be weakened** — it must deliver at least what the supertype promised. - **Invariants must be preserved** — supertype guarantees (immutability, ordering, non-empty results) still hold. - **The history constraint** — a subtype may not permit state changes the supertype's contract forbids (the canonical mutable-`Square`-extends-`Rectangle` failure). - **No new checked/unexpected exceptions** outside the declared contract. Much of this contract is *not expressible in the type system*; it lives in documentation and tests. Hence: write a **contract test suite** parameterized over every implementation. That test *is* the executable form of LSP. ## 3. Ways a polymorphic design still fails OCP 1. **Leaky abstraction.** The interface exposes variant-specific accessors (`isCard()`, `getIban()`), so callers branch on them. The switch is now spelled with getters. 2. **Callers downcast.** Any `instanceof` + cast in a consumer means the abstraction is missing an operation, or the wrong operations were chosen. 3. **Fat base class.** A shared abstract class accumulates helper state that each new variant must edit or fight. New variants modify existing code → not closed. 4. **Parallel hierarchies.** `PaymentMethod` variants are mirrored by `PaymentValidator`, `PaymentRenderer`, `PaymentAuditor` hierarchies, each with its own factory. Adding a variant now means four classes and four registrations — extension is additive but not *cheap*, and forgetting one fails at runtime. 5. **Wrong variation axis.** You subtyped by payment *method*, but the real churn is by *jurisdiction*. Every regulatory change touches all variants. PV protected the wrong seam — and this is the most expensive mistake, because the hierarchy actively obstructs the change that actually arrives. 6. **The enumeration escapes.** Config files, DB constraints, API enums, and UI dropdowns still enumerate variants, so "one new class" is really a coordinated multi-system change. OCP holds in the code and fails in the system. ## 4. Ways it fails LSP subtly - A `ReadOnlyCollection` implementing a mutable `Collection` interface and throwing on `add`. - A subtype that returns `null`/empty where the supertype promised a value. - A subtype with different *performance* characteristics that the contract implicitly relied on (a `List` implementation with O(n) indexing used in an indexed loop) — not a type error, but a real substitutability failure. - A subtype that requires initialization order or extra lifecycle calls the base didn't. - Overriding a method to do nothing ("harmless no-op") — silently weakens a postcondition and produces the hardest class of bug: no error, wrong behavior. The design responses: **split the interface** (Interface Segregation — `Refundable` separate from `PaymentMethod`), or make the capability queryable *as part of the contract* (`supportsRefund(): Boolean` documented on the supertype, so branching on it is legitimate contract use rather than a type test), or use a **Null Object** where a genuine do-nothing variant is semantically correct. ## 5. Where the principles disagree OCP-maximalism ("never modify existing code") pushes toward speculative abstraction: interfaces with one implementation, factories for everything, plugin machinery for variation that never arrives. PV explicitly guards against this by qualifying the target as **predicted** variation — evidence-based, not imagined. YAGNI is the counterweight; the mature stance is to add the abstraction when the second real variant appears, at which point the axis of variation is observed rather than guessed. Similarly, LSP sometimes argues *against* a hierarchy: if the candidate subtype cannot honor the contract, the answer is not a weaker contract — it is two separate types, or composition instead of inheritance. ## 6. Interview-ready synthesis "Protected Variations tells me *where* to put a seam. Polymorphism is the mechanism I usually use for that seam. If both are done well the code is Open–Closed. Liskov is the invariant I must not break, and the way I enforce it is a contract test that every implementation must pass. When I see an `instanceof` in a caller or an `UnsupportedOperation` in an implementation, I know the seam is in the wrong place or the interface is the wrong shape."

  • How do you actually enforce LSP in a codebase, given the compiler can't check most of it?
    A shared contract test suite parameterized over every implementation of the interface, asserting the supertype's documented pre/postconditions and invariants; new implementations are wired into it by default. Property-based tests are especially effective for invariants. Code review then treats `instanceof` in consumers and `UnsupportedOperation` in implementations as automatic findings.
  • When is branching on a capability flag like `supportsRefund()` acceptable, and when is it just a disguised type check?
    Acceptable when the capability is part of the supertype's documented contract and the flag's semantics are uniform across implementations — callers are then using the interface, not guessing at types. It is a disguised type check when the flag exists solely to identify one variant, correlates one-to-one with a class, or is paired with a downcast.
  • Where does Interface Segregation fit into this?
    It is the usual repair for LSP violations caused by fat interfaces. If several implementations cannot honor part of the contract, split it (`Refundable`, `Recurring`, `Chargeable`) so each implementation only promises what it can deliver, and callers depend on the narrowest interface they need.

A wall socket is Protected Variations: appliances vary, the socket is stable. Polymorphism is the plug standard that implements it. Open–Closed is being able to buy a new appliance without rewiring the house. Liskov is the rule that a plug drawing 3 kW must not be shipped with a 5 A plug — physically compatible, contractually not, and the failure is a fire, not a compile error.

saying these in an interview costs you the question

  • Treating the four principles as interchangeable — PV is the goal, Polymorphism the mechanism, OCP the property, LSP the constraint.
  • Believing that having an interface automatically gives Open–Closed; parallel hierarchies, fat base classes, and external enumerations break it while the interface still exists.
  • "Throwing UnsupportedOperationException is fine, it documents intent" — it strengthens a precondition and forces callers back into type checks.
  • Assuming the compiler enforces LSP; most of the contract (invariants, postconditions, performance expectations) lives in docs and tests.
  • Abstracting every axis 'for OCP' — PV targets *predicted* variation, and speculative interfaces with one implementation are pure cost.
  • Overriding a method as a silent no-op and calling it harmless.

context