askill
liskov-substitution-principle

liskov-substitution-principleSafety 100Repository

Use when creating subclasses or implementing interfaces. Use when tempted to override methods with exceptions or no-ops. Use when inheritance hierarchy feels wrong.

5 stars
1.2k downloads
Updated 1/22/2026

Package Files

Loading files...
SKILL.md

Liskov Substitution Principle (LSP)

Overview

Subtypes must be substitutable for their base types without altering program correctness.

If S is a subtype of T, objects of type T can be replaced with objects of type S without breaking the program. Subclasses must honor the contracts of their parent classes.

When to Use

  • Creating a class that extends another class
  • Overriding methods from a parent class
  • Implementing an interface
  • Feeling like you need to throw exceptions in overridden methods
  • Inheritance hierarchy feels "forced"

The Iron Rule

NEVER create a subclass that breaks the expectations of the parent class.

No exceptions:

  • Not for "it's the standard approach"
  • Not for "I'll note it as an anti-pattern"
  • Not for "the requirements say to extend"
  • Not throwing exceptions in overridden methods
  • Not making overridden methods no-ops

Providing violating code "with a caveat" is still providing violating code.

Detection: The Substitution Test

Ask: "Can I replace every instance of Parent with Child without breaking anything?"

function processRectangle(rect: Rectangle): void {
  rect.setWidth(5);
  rect.setHeight(10);
  assert(rect.getArea() === 50); // Always true for Rectangle
}

// If Square extends Rectangle:
const square = new Square(5);
processRectangle(square); // FAILS! Area is 100, not 50

If substitution breaks code, you have an LSP violation.

Detection: Override Smells

These overrides indicate LSP violations:

// ❌ VIOLATION: Throwing in override
class Penguin extends Bird {
  fly(): void {
    throw new Error("Penguins can't fly"); // Breaks callers expecting fly()
  }
}

// ❌ VIOLATION: No-op override
class ReadOnlyStorage extends FileStorage {
  write(path: string, content: string): void {
    // Silently does nothing - breaks caller expectations
  }
}

// ❌ VIOLATION: Changing behavior semantics
class Square extends Rectangle {
  setWidth(w: number): void {
    this.width = w;
    this.height = w; // Changes height too - breaks expectations
  }
}

The Correct Pattern: Composition & Interfaces

Don't force inheritance. Use interfaces to define capabilities.

Square/Rectangle Problem

// ✅ CORRECT: Separate types, shared interface
interface Shape {
  getArea(): number;
}

class Rectangle implements Shape {
  constructor(private width: number, private height: number) {}
  getArea(): number { return this.width * this.height; }
  setWidth(w: number): void { this.width = w; }
  setHeight(h: number): void { this.height = h; }
}

class Square implements Shape {
  constructor(private size: number) {}
  getArea(): number { return this.size * this.size; }
  setSize(s: number): void { this.size = s; }
}

Bird/Penguin Problem

// ✅ CORRECT: Capability interfaces
interface Flyable {
  fly(): void;
}

abstract class Bird {
  abstract eat(): void;
}

class Sparrow extends Bird implements Flyable {
  eat(): void { /* ... */ }
  fly(): void { /* ... */ }
}

class Penguin extends Bird {
  eat(): void { /* ... */ }
  swim(): void { /* ... */ }
  // No fly() - doesn't promise what it can't deliver
}

ReadOnly Problem

// ✅ CORRECT: Separate interfaces
interface Readable {
  read(path: string): string;
}

interface Writable {
  write(path: string, content: string): void;
  delete(path: string): void;
}

class FileStorage implements Readable, Writable {
  read(path: string): string { /* ... */ }
  write(path: string, content: string): void { /* ... */ }
  delete(path: string): void { /* ... */ }
}

class AuditLogStorage implements Readable {
  read(path: string): string { /* ... */ }
  // No write/delete - doesn't extend something it can't honor
}

Pressure Resistance Protocol

1. "Just Override and Throw"

Pressure: "Handle the fact they can't fly by throwing an error"

Response: Throwing in an override violates the contract. Code expecting fly() will crash.

Action: Restructure with interfaces. Don't inherit methods you can't honor.

2. "It's the Standard Approach"

Pressure: "Override-and-throw is the standard way to do this"

Response: "Standard" doesn't mean correct. This pattern causes runtime failures.

Action: Use composition and interfaces instead.

3. "The Requirements Say Extend"

Pressure: "Square must extend Rectangle per the requirements"

Response: Requirements that mandate LSP violations are wrong. Push back.

Action:

"A Square extending Rectangle violates LSP and will cause bugs.
I recommend: [correct approach with interfaces].
Should I implement the correct structure, or document this as known tech debt?"

4. "I'll Note It's an Anti-Pattern"

Pressure: Internal rationalization

Response: Providing bad code with a caveat is still providing bad code.

Action: Provide only the correct solution. Don't implement the violation.

Red Flags - STOP and Reconsider

If you notice ANY of these, you're about to violate LSP:

  • Overriding a method to throw an exception
  • Overriding a method to do nothing (no-op)
  • Overriding a method to change its fundamental behavior
  • Subclass can't do everything the parent can
  • Inheritance feels forced or unnatural
  • Using instanceof checks to handle subtypes differently

All of these mean: Use composition and interfaces instead.

Quick Reference

ViolationCorrect Approach
Square extends RectangleBoth implement Shape interface
Penguin extends Bird (with fly)Bird base + Flyable interface
ReadOnlyStorage extends StorageSeparate Readable/Writable interfaces
Child throws in overrideChild shouldn't extend that parent
Child no-ops an overrideChild shouldn't extend that parent

Common Rationalizations (All Invalid)

ExcuseReality
"It's the standard approach"Common doesn't mean correct.
"I provided a caveat"Bad code with warnings is still bad code.
"Requirements say extend"Requirements can be wrong. Push back.
"Throwing makes it explicit"Throwing breaks callers. Compile errors are better.
"No-op is safe"Silent failures hide bugs.
"It's just for this one case"One violation leads to more. Fix it properly.

The Bottom Line

If a subclass can't fully substitute for its parent, don't use inheritance.

Use interfaces to define capabilities. Use composition to share behavior. Never override methods with exceptions or no-ops.

When asked to create violating inheritance: restructure with interfaces instead. Don't provide the violation "with a caveat."

Install

Download ZIP
Requires askill CLI v1.0+

AI Quality Score

95/100Analyzed 4/4/2026

Excellent skill on Liskov Substitution Principle with comprehensive coverage of violations, detection methods, and correct patterns. Features TypeScript code examples, the Substitution Test, Override Smells, and practical Pressure Resistance Protocol. Well-structured with tables and clear indicators. Bonus rules apply (when to use, structured steps, dedicated skills folder, high-density technical content). Minor tag limitation (only 'testing').

100
95
90
95
95

Metadata

Licenseunknown
Version-
Updated1/22/2026
Publisheryanko-belov

Tags

testing