Skip to main content

Liskov Substitution Principle (LSP)

The Liskov Substitution Principle (LSP) is a fundamental concept in object-oriented programming, forming one of the SOLID principles. It states that objects of a superclass should be replaceable with objects of a subclass without altering the correctness of the program. This principle ensures that a subclass behaves in a manner consistent with the expectations set by its superclass.

What is the Liskov Substitution Principle?​

Shubh: Do you know about the Liskov Substitution Principle?

You: I’ve heard of it, but could you explain it a bit more?

Shubh: Sure! The Liskov Substitution Principle (LSP) states that objects in a program should be replaceable with instances of their subtypes without altering the correctness of that program. Essentially, subclasses should be able to stand in for their parent classes without causing any issues.

Why is LSP Important?​

Shubh: The LSP helps maintain the integrity and reliability of a system by ensuring that subclasses can stand in for their parent classes without causing unexpected issues. This principle supports the creation of robust, maintainable, and flexible codebases by encouraging proper inheritance hierarchies.

Example: Violation of LSP (Bad Approach)​

Let's look at an example where LSP is violated:

JAVA
public class Creature {
void fly() throws Exception {
System.out.println("Creature is flying");
}

void makeNoise() {
System.out.println("Creature is making noise");
}
}

public class Eagle extends Creature {
@Override
public void fly() {
System.out.println("Eagle is flying");
}

@Override
public void makeNoise() {
System.out.println("Eagle is making noise");
}
}

public class Parrot extends Creature {
@Override
public void fly() {
System.out.println("Parrot is flying");
}

@Override
public void makeNoise() {
System.out.println("Parrot is making noise");
}
}

public class Cow extends Creature {
@Override
public void fly() throws Exception {
throw new Exception("Cow cannot fly");
}

@Override
public void makeNoise() {
System.out.println("Cow is making noise");
}
}

public class Main {
public static void main(String[] args) throws Exception {
Creature creature = new Eagle();
creature.fly(); // Works as expected

creature = new Cow();
creature.fly(); // Can call fly method but it throws Exception
}
}

What's Wrong Here?​

You: So, what's wrong with this code?

Shubh: In the example above, the Cow class violates the Liskov Substitution Principle because it throws an exception when the fly method is called. This breaks the expected behavior defined by the Creature superclass, where all creatures are assumed to be capable of flying.

Correct Approach (Adhering to LSP)​

To adhere to LSP, we need to separate creatures that can fly from those that cannot. This can be done by creating a more specific hierarchy:

JAVA
public class Creature {
void makeNoise() {
System.out.println("Creature is making noise");
}
}

public class FlyingCreature extends Creature {
void fly() {
System.out.println("Creature is flying");
}
}

public class Eagle extends FlyingCreature {
@Override
public void fly() {
System.out.println("Eagle is flying");
}

@Override
public void makeNoise() {
System.out.println("Eagle is making noise");
}
}

public class Parrot extends FlyingCreature {
@Override
public void fly() {
System.out.println("Parrot is flying");
}

@Override
public void makeNoise() {
System.out.println("Parrot is making noise");
}
}

public class Cow extends Creature {
@Override
public void makeNoise() {
System.out.println("Cow is making noise");
}
}

public class Main {
public static void main(String[] args) {
FlyingCreature flyingCreature = new Eagle();
flyingCreature.fly(); // Works as expected

Creature creature = new Cow();
// Cannot call fly() method, adhering to LSP
creature.makeNoise(); // Works as expected
}
}

Key Points of the Correct Approach​

Shubh: In the refactored code:

  • Separation of Concerns: The FlyingCreature class ensures that only flying creatures can invoke the fly method, while non-flying creatures like Cow do not have this method.

  • Safe Substitution: Any instance of FlyingCreature can be safely replaced with any of its subclasses (Eagle or Parrot) without breaking the expected behavior.

  • Clarity and Maintainability: The code is clearer and adheres to proper object-oriented design principles, making it easier to maintain and extend.

You: That makes a lot of sense! By making sure the hierarchy is correctly defined, we ensure that the program behaves predictably and correctly.

Visual Representation​

Liskov Substitution

Illustration of the Liskov Substitution Principle

I hope that this example clarifies the importance of adhering to LSP. By ensuring that subclasses can stand in for their parent classes without causing unexpected behavior, we create more robust and maintainable systems.