Skip to content

LSP in Practice

When to Check LSP

During code review, check LSP when: - Creating subclasses or implementing interfaces - Overriding methods - Using polymorphism - Seeing unexpected behavior with subtypes

Pattern: Detecting LSP Violations

Contract Testing (Python example):

# Base contract test
class BankAccountTests:
    def test_withdraw_reduces_balance(self, account):
        initial = account.balance
        account.withdraw(10)
        assert account.balance == initial - 10

    def test_withdraw_never_goes_negative(self, account):
        account.withdraw(account.balance + 1)
        assert account.balance >= 0

# All subtypes must pass base tests
class CheckingAccountTests(BankAccountTests):
    def setup(self):
        self.account = CheckingAccount(100)

class SavingsAccountTests(BankAccountTests):
    def setup(self):
        self.account = SavingsAccount(100)

# If FixedDepositAccount can't withdraw, it violates LSP
class FixedDepositAccount(BankAccount):
    def withdraw(self, amount):
        raise NotImplementedError("Fixed deposits can't withdraw")
    # LSP VIOLATION: breaks base contract

LSP-Compliant Solution:

# Separate hierarchies
class Account:
    def deposit(self, amount): pass

class WithdrawableAccount(Account):
    def withdraw(self, amount): pass

class CheckingAccount(WithdrawableAccount):
    def withdraw(self, amount): # implements withdrawal

class FixedDepositAccount(Account):
    # No withdraw method: LSP preserved

Pattern: Covariance and Contravariance

PHP 7.4+ Example:

interface AnimalFeeder {
    public function feed(Animal $animal): Food;
}

// LSP-compliant covariance/contravariance
class CatFeeder implements AnimalFeeder {
    // Contravariance: accept broader input (Animal or subtype)
    public function feed(Animal $animal): CatFood {
        // Covariance: return narrower output (Food subtype)
        return new CatFood();
    }
}

Decision: Composition Over Inheritance

Scenario Use Inheritance Use Composition Why
True "is-a" relationship Yes Car is-a Vehicle (passes LSP)
Behavioral substitution required Yes Payment methods implement same contract
Code reuse only Yes Stack uses ArrayList, isn't-a ArrayList
Multiple behaviors Yes Avoid multiple inheritance issues

Pattern: Refactoring LSP Violations

TypeScript Example:

// Violation: ReadOnlyList inherits from List
class List<T> {
  add(item: T) { /* adds item */ }
  remove(index: number) { /* removes item */ }
  get(index: number): T { /* returns item */ }
}

class ReadOnlyList<T> extends List<T> {
  add(item: T) { throw new Error("Read-only"); } // LSP VIOLATION
  remove(index: number) { throw new Error("Read-only"); } // LSP VIOLATION
}

// LSP-Compliant: separate hierarchies
interface IReadable<T> {
  get(index: number): T;
}

interface IWritable<T> extends IReadable<T> {
  add(item: T): void;
  remove(index: number): void;
}

class List<T> implements IWritable<T> {
  add(item: T) { /* ... */ }
  remove(index: number) { /* ... */ }
  get(index: number): T { /* ... */ }
}

class ReadOnlyList<T> implements IReadable<T> {
  get(index: number): T { /* ... */ }
}

Common Mistakes

  • Implementing interfaces you can't fulfill -- Adding NotImplementedError methods. Why: violates LSP contract
  • Returning null when base returns object -- Weakens postcondition. Why: clients don't expect null
  • Throwing exceptions base doesn't throw -- Violates exception contract. Why: clients can't handle unexpected exceptions
  • Making subtype more restrictive -- Requiring non-null when base allows null. Why: breaks existing clients
  • Changing semantics of overridden methods -- save() persists in base, caches in subtype. Why: violates behavioral contract
  • Performance surprises -- O(1) base method becomes O(n) in subtype. Why: violates implicit performance expectations

Testing for LSP Compliance

Automated testing approach: 1. Write contract tests for base type 2. Run same tests against all subtypes 3. If subtype fails base tests -- LSP violation

Static analysis (TypeScript strict mode): - Enable strictFunctionTypes for parameter contravariance checking - Enable strictNullChecks to catch null contract violations