From grimoire
Evaluates inheritance decisions using behavioral substitutability (LSP), composition-over-inheritance, and fragile base class awareness. Activates when class extension, instanceof checks, or contract-weakening overrides appear.
How this skill is triggered — by the user, by Claude, or both
Slash command
/grimoire:apply-inheritance-correctlyThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Use inheritance only when a subtype is behaviorally substitutable for its parent — verify the IS-A relationship holds under LSP, not just syntactically.
Use inheritance only when a subtype is behaviorally substitutable for its parent — verify the IS-A relationship holds under LSP, not just syntactically.
Adopted by: Java (final classes by default in Kotlin, records in Java 16+), C++ (non-virtual by default), Swift (inheritance restricted, protocols preferred), Go (no class inheritance — structural typing only). Joshua Bloch's "Effective Java" dedicates three items to inheritance misuse and opens Item 19 with: "Design and document for inheritance or else prohibit it."
Impact: The "fragile base class" problem — where a change to a superclass breaks subclasses that the author did not anticipate — is one of the most frequently cited causes of regression in OOP codebases. The GoF note in "Design Patterns" that most design patterns exist to replace inappropriate inheritance with composition. A survey of Java open-source projects found that >60% of inheritance hierarchies deeper than 2 levels contained at least one LSP violation — most commonly overrides that narrowed postconditions or threw exceptions the parent did not declare.
Why best: The alternative — composition — is almost always safer: it avoids the fragile base class problem, supports multiple behaviors without deep hierarchies, and can be changed at runtime. Inheritance should be reserved for genuine IS-A relationships where behavioral substitutability is provable, not convenient.
Sources: Liskov & Wing, "A behavioral notion of subtyping" (ACM TOPLAS, 1994); Gamma et al., "Design Patterns" (1994); Bloch, "Effective Java, 3rd ed." (Addison-Wesley, 2018); Meyers, "Effective C++, 3rd ed." (2005)
Inheritance is correct when: every operation valid on the parent is valid on the subtype, with the same contract.
Ask: "Can I replace every use of Parent with Child without breaking the caller?"
# IS-A passes: Dog IS-A Animal; all Animal behaviors apply
class Animal:
def breathe(self): ...
def eat(self): ...
class Dog(Animal):
def breathe(self): ... # still breathes
def eat(self): ... # still eats
def fetch(self): ... # additional behavior only
If the answer is "it depends" or "mostly" — use composition.
LSP requires three properties:
| Property | Rule |
|---|---|
| Preconditions | Subtype must not strengthen input requirements |
| Postconditions | Subtype must not weaken output guarantees |
| Invariants | Subtype must preserve all invariants the parent establishes |
# Bad — Square violates Rectangle's contract (postcondition weakened)
class Rectangle:
def set_width(self, w: float) -> None:
self.width = w # height unchanged — caller assumes this
class Square(Rectangle):
def set_width(self, w: float) -> None:
self.width = w
self.height = w # silently changes height — breaks caller
# Good — separate types; share abstraction only if behavior is truly common
class Shape(ABC):
@abstractmethod
def area(self) -> float: ...
class Rectangle(Shape):
def area(self): return self.width * self.height
class Square(Shape):
def area(self): return self.side ** 2
Inheriting from a concrete class creates the fragile base class problem: any change to the parent — even adding a helper method — can break subclasses if they override a method the parent now calls.
// Risky — HashSet is concrete; overriding is fragile
public class CountingSet<E> extends HashSet<E> {
private int addCount = 0;
@Override
public boolean add(E e) {
addCount++;
return super.add(e); // addAll() also calls add() — double count bug
// (Bloch's classic example from Effective Java)
}
}
// Safe — compose, don't extend
public class CountingSet<E> {
private final Set<E> set = new HashSet<>();
private int addCount = 0;
public boolean add(E e) {
addCount++;
return set.add(e);
}
}
If you have not explicitly designed a class for inheritance — documented its overridable methods, their contracts, and their call sequences — mark it final (Java/Kotlin), sealed (C#), or add a comment that it is not designed for subclassing.
// Kotlin: final is the default; open is explicit
open class Animal { ... } // designed for inheritance
class Dog : Animal() { ... } // OK — Animal was opened intentionally
data class Point(val x: Int, val y: Int) // sealed — not for inheritance
Every protected or public non-final method is an API contract for subclasses. Document: what it does, when it is called, what preconditions it assumes, and what postconditions the override must maintain.
protected method is an API contract: once overridable, it cannot safely changeinstanceof or typeof checks in calling code are a LSP violation signalfinal / sealed by default; open for extension only when you have designed for itScenario: A team creates AdminUser extends User to reuse User's authentication logic. Over time, AdminUser overrides canAccess() to always return true. Code that calls user.canAccess(resource) breaks when an AdminUser bypasses access checks the caller relies on. LSP is violated — switching to composition (AdminUser holds a User and delegates authentication) fixes the contract breach.
Scenario: A ReadOnlyList extends ArrayList overrides add(), remove(), and clear() to throw UnsupportedOperationException. Callers that receive a List cannot know it will throw on mutation. The correct design is to implement List<T> directly, making the read-only contract explicit at the type level.
Scenario: A library provides BaseController with handleRequest() that logs, authenticates, then calls protected abstract processRequest(). A subclass overrides handleRequest() entirely to skip authentication. The next version of BaseController adds rate-limiting inside handleRequest() — the subclass bypasses it. Sealing handleRequest() (making it final) and only exposing processRequest() as the extension point prevents the bypass.
class EmailService extends DatabaseService to get DB helpers. Use composition or inject a collaborator.NotImplementedException — signals the base class has too broad an interface; apply ISP instead.instanceof after the fact — if you need to know the concrete type at a call site, the abstraction is wrong or LSP is violated.TestCase, AbstractController), inheritance is the designed mechanism; LSP still applies, but the decision to inherit is made by the frameworkActivity) require inheritance; apply LSP diligently but don't fight the platformnpx claudepluginhub jeffreytse/grimoire --plugin grimoireGuides applying composition over inheritance to reduce tight coupling and improve maintainability in OOP systems.
Evaluates class and routine design against Code Complete rules: LSP inheritance, parameter count limits, cohesion classification, containment-vs-inheritance.
Guides choosing inheritance vs composition in OOP for class hierarchies and object composition. Includes Java examples like Vehicle/Car.