Skip to main content
Create your own
Lesson illustration

Applying the Liskov Substitution Principle (LSP)

Hello! In our last lesson, we focused on the Open/Closed Principle (OCP), which guides us to build systems that are open for extension but closed for modification. We achieved this by using abstractions, like Java interfaces, allowing us to add new functionality by plugging in new implementations without changing existing client code.

Today, we address a crucial follow-up question: If we can substitute different implementations of an interface, how do we guarantee those new implementations won't break the application? This brings us to the third SOLID principle, the Liskov Substitution Principle (LSP).

This lesson addresses the learning outcome: Apply the Liskov Substitution Principle (LSP) to ensure subtype substitutability.

LSP provides the rules for creating "correct" inheritance hierarchies. It ensures that a subclass can be used in place of its superclass without causing unexpected behavior. Mastering LSP is what makes the Open/Closed Principle truly robust and is essential for designing reliable, object-oriented systems.

1. What is the Liskov Substitution Principle?

The principle was defined by computer scientist Barbara Liskov. In simple terms, it states:

Subtypes must be substitutable for their base types.

This means if you have a piece of code that is designed to work with a superclass object, it should continue to work correctly if you pass it an object of any of its subclasses, without the code needing to know the specific subclass it's dealing with.

Let's start with a short video that introduces the concept and its importance.

Low Level Design 107 | Liskov Substitution Principle | 2022 | System Design

This video from sudoCODE provides a great introduction to the Liskov Substitution Principle, explaining its formal definition in simple terms and why it's a critical guide for proper inheritance.

Watch the first 2 minutes and 25 seconds of the video. Focus on the core idea: a function that takes a base class instance should also work with a derived class instance without breaking.

To visualize a correct application of LSP, consider this example:

Liskov Substitution Principle with Vehicle Hierarchy
This diagram shows a `Vehicle` superclass and two subclasses, `Car` and `Bus`. A client that expects a `Vehicle` object can be given either a `Car` or a `Bus`, and it can still call methods like `getSpeed()` without any issues. The subtypes are correctly substituting the supertype.

2. A Classic Violation: The Rectangle/Square Problem

One of the most famous examples of an LSP violation is the relationship between a rectangle and a square. In mathematics, a square is a rectangle. This might lead you to model the Square class as a subclass of the Rectangle class. However, this seemingly logical choice breaks LSP due to behavioral differences.

Let's explore this classic problem in detail.

Liskov Substitution Principle by Example

This resource from Technische Universität Dresden explains the Rectangle/Square problem clearly, showing how a client's assumptions about a base class can be broken by a subclass, even if the types are compatible.

Please read the sections 'Liskov Substitution Principle by Example' and 'Rectangles and Square - LSP Compliant Solution'. Pay close attention to the client code's assert statement and why it fails when a Square is used. Notice how the LSP-compliant solution resolves this by changing the class hierarchy.

As the article demonstrates, the problem lies in the behavior of the setters. A client of Rectangle reasonably assumes that setting the width does not affect the height.

void clientMethod(Rectangle rec) {
    rec.setWidth(5);
    rec.setHeight(4);
    // This assertion holds true for a Rectangle but fails for a Square.
    assert(rec.area() == 20); 
}

A Square subclass, to maintain its "squareness," must set both width and height to the same value in its setters. This violates the client's assumption. The Square object, while technically a Rectangle by type, does not behave like one. This is the essence of an LSP violation.

The correct design, as shown, is to make Rectangle and Square siblings, perhaps under a more general Shape abstraction that makes no promises about the independent behavior of its dimensions.

3. A Practical Violation: The Banking Application

The Rectangle/Square problem is a good academic example, but let's look at a scenario you are more likely to encounter in business applications. This often happens when a subclass cannot fulfill all the responsibilities of its superclass.

The most common code smell for this is a subclass method that throws an UnsupportedOperationException.

Let's walk through an excellent example from Baeldung involving a banking application.

Liskov Substitution Principle in Java

This article from Baeldung demonstrates how a design that follows the Open/Closed Principle can inadvertently violate LSP when a new requirement is introduced. It provides a very practical refactoring example.

Read sections 3 ('An Example Use Case') and 5 ('Refactoring'). In section 3, follow the story of adding a FixedTermDepositAccount. Note why it cannot implement the withdraw method and why throwing an exception breaks the client BankingAppWithdrawalService. In section 5, study the refactoring. See how introducing a new WithdrawableAccount abstraction resolves the LSP violation.

This banking example is fantastic because it's a realistic problem. The initial design, with an abstract Account class, seems perfect for OCP. But when a FixedTermDepositAccount is added, which doesn't support withdrawals, the hierarchy is broken from a behavioral perspective.

Forcing a subclass to implement a method it cannot support is a clear violation of LSP. The solution isn't to make the client code handle the exception; it's to fix the abstraction. By creating a more specific WithdrawableAccount that contains the withdraw method, the hierarchy becomes correct:

  • Account (can only deposit)
    • FixedTermDepositAccount
    • WithdrawableAccount (can also withdraw)
      • SavingsAccount
      • CurrentAccount

Now, the BankingAppWithdrawalService can safely depend on WithdrawableAccount, knowing that any object of this type is guaranteed to support withdrawals.

Liskov Substitution Principle Violation Example: Vehicle Hierarchy
This diagram illustrates the same problem. A `Bicycle` cannot substitute a `Vehicle` if the client expects to get fuel requirements, because a bicycle doesn't use fuel. The abstraction is wrong.
Test your understanding!

Imagine a User class in a system with a changePassword(String newPassword) method. You are asked to add support for guest users who log in via an external provider (e.g., "Sign in with Google") and do not have their own password.

You create a GuestUser class that extends User. How would you handle the changePassword method in the GuestUser class? What does this tell you about your design and LSP?

Show answer

A common but incorrect approach would be to override changePassword in GuestUser to either do nothing or throw an UnsupportedOperationException.

This is a classic LSP violation. A client function operating on a User object would expect to be able to call changePassword successfully. If it receives a GuestUser instance, the application might crash or fail silently.

This violation tells us that a GuestUser is not behaviorally a subtype of User. The abstraction is flawed. A better design would be to rethink the hierarchy. Perhaps there could be a base Principal or AccountHolder class without password logic. Then, you could have subclasses like LocalUser (which has a password) and FederatedUser (which does not). This is analogous to the Account vs. WithdrawableAccount refactoring.

4. The Rules of Behavioral Subtyping

So, how can we be sure our subclasses are "well-behaved"? LSP introduces the concept of behavioral subtyping, which goes beyond the syntactic checks of the Java compiler. It's guided by a set of rules, often framed as a "contract" between a class and its clients.

A subclass must honor the contract of its superclass. This contract includes:

  • Preconditions: Conditions that must be true before a method is executed. A subclass cannot strengthen preconditions. It can only make them the same or weaker.
  • Postconditions: Conditions that must be true after a method is executed. A subclass cannot weaken postconditions. It can only make them the same or stronger.
  • Invariants: Conditions that must always remain true for an object's state. Subclasses must preserve the invariants of the superclass.

Liskov Substitution Principle in Java

The Baeldung article continues with a detailed breakdown of these rules, providing clear Java examples for each one.

Read section 6, 'Rules'. This is the most technical part of the lesson, but it's crucial for truly understanding how to apply LSP. Focus on the definitions and examples for preconditions, postconditions, and invariants.

These rules ensure that a subclass doesn't surprise a client. For example, if a superclass method works for any positive number (precondition), a subclass can't restrict it to only work for numbers greater than 10 (strengthening the precondition). This would break a client that tries to pass the number 5.

5. Spotting and Fixing Violations

You are now equipped with the theory. Let's focus on identifying LSP violations in practice. Watch for these code smells:

  1. A subclass method throws UnsupportedOperationException or a similar exception for a behavior it cannot perform.
  2. A subclass provides an empty implementation for an inherited method.
  3. Client code uses instanceof or type casting to check for a specific subclass before calling a method. This means the client knows the abstraction is leaky.

This final video provides one more example of spotting an LSP violation and fixing it, this time by preferring composition over inheritance—a principle we've discussed before.

SOLID Design Principles with Java Examples | Clean Code and Best Practices | Geekific

This clip from Geekific shows another common violation, where a PremiumVideo can't fulfill the playRandomAd() behavior of its Video superclass. Notice how the solution shifts from an 'is-a' (inheritance) to a 'has-a' (composition) relationship.

Watch from 05:45 to 07:40. Observe the problem with the PremiumVideo subclass and how creating a VideoManager class to hold the behaviors allows each video type to cherry-pick what it needs, thus adhering to LSP.

Conclusion

The Liskov Substitution Principle is the safety net for the Open/Closed Principle. It ensures that the new extensions we create are behaviorally sound, leading to more reliable and predictable systems.

Key Takeaways:

  • Substitutability is Key: If client code expects an object of type T, you should be able to pass it an object of any subtype S of T without the client code breaking.
  • Behavior Over "Is-A": A subclass must not just be a type-compatible substitute but also a behaviorally-compatible one. The Rectangle/Square problem is the classic example of this distinction.
  • Honor the Contract: Subclasses must adhere to the superclass's contract, meaning they cannot strengthen preconditions or weaken postconditions.
  • Watch for Smells: Empty method overrides, UnsupportedOperationException, and instanceof checks are strong indicators of LSP violations. Fixing these often requires rethinking your abstractions.

In our next lesson, we'll move on to the Interface Segregation Principle (ISP). Now that we understand how to build correct class hierarchies, ISP will teach us how to design interfaces that are lean, focused, and tailored to the needs of their clients.

Can't find a good explanation? Sign up and we'll make it for you

Sign up