Liskov Substitution Principle – Simple Explanation with Example

As our applications grow, we often create new classes by extending existing classes. This is useful because we can reuse code and create relationships between different types of objects.

But there is an important question we should ask:

Can the child class really be used wherever the parent class is expected?

If a child class changes the behavior expected from its parent, it can cause unexpected problems in our application.

The Liskov Substitution Principle (LSP) helps us avoid this problem.

Liskov Substitution Principle

What is the Liskov Substitution Principle?

The idea is simple:

A child class should be usable wherever its parent class is expected without breaking the program.

In simple words, if a class extends another class, the child should follow the behavior that the parent promises.

This allows us to replace the parent object with a child object without unexpected problems.

Why Does LSP Matter?

When a child class does not behave as expected:

  • Existing code can break
  • Unexpected errors can occur
  • Extra conditions may be required
  • The code becomes harder to maintain

Instead, child classes should be able to safely replace their parent classes.

Bad Example (Breaking the Parent's Behavior)

Let's imagine we have a payment system.

We create a parent class called Payment that provides a method for making a payment:

class Payment {
    pay(amount) {
        console.log(`Paid ${amount}`);
    }
}

Now we create different payment methods by extending this class:

class CardPayment extends Payment {
    pay(amount) {
        console.log(`Paid ${amount} using card`);
    }
}

class CashPayment extends Payment {
    pay(amount) {
        console.log(`Paid ${amount} using cash`);
    }
}

Both child classes behave as expected.

But imagine we create another child class that cannot actually make a payment:

class FreePayment extends Payment {
    pay(amount) {
        throw new Error("Payment is not required");
    }
}

Problem here:

  • Payment expects pay() to perform a payment
  • FreePayment throws an error when pay() is called
  • The child class does not behave as the parent class promises

This means FreePayment cannot safely replace Payment.

How Does This Break the Code?

Imagine a function that accepts any payment method:

function processPayment(payment, amount) {
    payment.pay(amount);
}

With a normal payment class, this works:

const cardPayment = new CardPayment();

processPayment(cardPayment, 100);

But if we pass FreePayment:

const freePayment = new FreePayment();

processPayment(freePayment, 100);

The function throws an error because the child class does not provide the behavior that the parent promised.

This is the kind of problem LSP helps us prevent.

Good Example (Following LSP)

Instead of creating child classes that break the behavior of the parent, we should make sure that every child class properly follows the expected behavior.

class Payment {
    pay(amount) {
        throw new Error("Method must be implemented");
    }
}

class CardPayment extends Payment {
    pay(amount) {
        console.log(`Paid ${amount} using card`);
    }
}

class CashPayment extends Payment {
    pay(amount) {
        console.log(`Paid ${amount} using cash`);
    }
}

Now both child classes correctly support the pay() operation.

We can use them through the same function:

function processPayment(payment, amount) {
    payment.pay(amount);
}

const cardPayment = new CardPayment();
const cashPayment = new CashPayment();

processPayment(cardPayment, 100);
processPayment(cashPayment, 200);

Why this is better:

  • Child classes follow the behavior expected from the parent
  • The calling code does not need to know the specific payment type
  • We can replace one payment implementation with another
  • The code becomes easier to maintain

The Main Idea Behind LSP

LSP is not simply about using the extends keyword.

It is about making sure that the child class respects the behavior of its parent.

If our code expects:

Payment → pay()

then a valid child class should be able to provide that behavior without unexpectedly breaking the program.

A Simple Real-Life Example

Think about a universal charger.

If a device is designed to support that charger, you expect the charger to work with it.

If another device looks compatible but suddenly refuses to work with the charger, it does not behave as expected.

In programming, the idea is similar:

  • Parent class → defines expected behavior
  • Child class → follows that behavior
  • Code using the parent → should continue working with the child

How to Recognize an LSP Problem

One warning sign is when the calling code constantly needs to check which child class it received:

if (payment instanceof CardPayment) {
    // do something
} else if (payment instanceof CashPayment) {
    // do something else
}

If we constantly need to handle each child class differently, it may indicate that the inheritance relationship is not designed properly.

The goal is to let the parent abstraction handle the common behavior without the calling code worrying about the specific child class.

Key Idea to Remember

  • Child classes should behave like their parent classes
  • A child should be safely replaceable by its parent
  • Do not create inheritance relationships that break expected behavior
  • Existing code should continue working when a child class is used

Conclusion

Inheritance is useful when a child class genuinely behaves like its parent.

But if a child class changes or breaks the behavior expected from the parent, it can make our application difficult to maintain.

By following the Liskov Substitution Principle, we can create inheritance relationships that are more predictable and reliable.

The core idea is simple:

If a child replaces its parent, the program should continue working as expected.

Summary

  • LSP stands for Liskov Substitution Principle
  • It is the third principle of SOLID
  • Child classes should be usable wherever their parent classes are expected
  • Child classes should not break the behavior expected from the parent
  • Good inheritance makes code easier to maintain and extend


Interface Segregation Principle – Simple Explanation with Example

When building applications, we often create classes or interfaces that contain many different methods. At first, this may seem convenient ...