liskov-substitution-principle

Liskov Substitution Principle (LSP)

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "liskov-substitution-principle" with this command: npx skills add yanko-belov/code-craft/yanko-belov-code-craft-liskov-substitution-principle

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.

  1. "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.

  1. "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?"

  1. "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

Violation Correct Approach

Square extends Rectangle Both implement Shape interface

Penguin extends Bird (with fly) Bird base + Flyable interface

ReadOnlyStorage extends Storage Separate Readable/Writable interfaces

Child throws in override Child shouldn't extend that parent

Child no-ops an override Child shouldn't extend that parent

Common Rationalizations (All Invalid)

Excuse Reality

"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."

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Coding

dont-repeat-yourself

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

keep-it-simple

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

lazy-loading

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

separation-of-concerns

No summary provided by upstream source.

Repository SourceNeeds Review