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?
answer
- PV = why, Polymorphism = how, OCP = result, LSP = precondition
- UnsupportedOperation ⇒ strengthened precondition ⇒ LSP break
- instanceof in caller ⇒ abstraction missing an operation
- contract test = executable LSP
- wrong variation axis = most expensive failure
basics
~20 sPolymorphism 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 sThey 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// 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
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.
Add the LSP failure mode — UnsupportedOperation and downcasting callers — and the Interface Segregation repair.
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.
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.