1. Overview

This lesson focuses on the “L” in SOLID: The Liskov Substitution Principle (LSP). Named after computer scientist Barbara Liskov, this principle is fundamental to creating reliable and correct inheritance hierarchies in object-oriented design. It’s the rule that ensures our abstractions are trustworthy.

This is a theory-focused lesson. We’ll define the principle and explore the core concept of “Design by Contract,” which provides the rules for following LSP in real-world systems. Finally, we’ll look at the most common ways this principle is violated.

There is no code we need to check out to follow along with this lesson.

2. What Is the Liskov Substitution Principle?

Let’s start with the formal definition of LSP: “In object-oriented programming, subtypes must be substitutable for their base types without altering the correctness of the program.”

In simpler terms, if we have a variable declared as a parent class type, we should be able to assign an object of any of its child classes to that variable, and the program should continue to work as expected. The key idea is that client code should not need to know which concrete subclass it is interacting with. It should rely only on the guarantees provided by the base type.

This principle is what makes polymorphism safe. Without LSP, polymorphism becomes a source of subtle bugs rather than a tool for clean design. Just to be clear, the principle only constrains the behavior promised by the base type. As long as the inherited methods continue to honor the original contract, adding new, more specific operations in a subclass does not violate LSP.

LSP is the “enabler” for the Open/Closed Principle (OCP).

  • OCP states that software should be open to extension but closed to modification, typically by adding new subclasses.
  • LSP ensures that these new subclasses can actually be used in place of the existing ones without forcing changes to existing code.

If a subclass violates LSP, a strong indicator is that we are forced to add defensive logic, such as if (obj instanceof BadSubclass) or special-case handling. This directly violates OCP and spreads knowledge of concrete implementations throughout the system. By following LSP, we ensure that abstractions remain stable and reusable over time.

3. The Rules: Design by Contract

The most practical way to understand and apply LSP is through the concept of “design by contract.” In this model, each method represents a contract between the class and its clients. When a subclass extends a parent class, it implicitly agrees to honor that contract.

This contract has three parts:

  • Preconditions: What must be true before a method is called? (for example, “the input must not be null”).
  • Postconditions: What does the method promise to be true after it runs? (for example, “the item will be saved to the database”).
  • Invariants: Conditions that are always true for the class’s state, for instance, “a list’s size is never negative”.

To follow LSP, a subclass’s methods must adhere to these rules:

  • They cannot strengthen preconditions – They can’t be stricter than the parent. If the parent accepts any positive number, the child cannot require the number to be greater than 100.
  • They cannot weaken postconditions – They can’t promise less than the parent. If the parent promises to save data, the child cannot silently fail to save.
  • They must preserve all invariants of the parent class – A subclass may introduce additional invariants, but it must never violate those established by the base class.

These rules ensure that a subclass is a true specialization rather than a behavioral deviation.

4. Recognizing Violations

The best way to understand LSP is to look at common ways it is broken. These “code smells” indicate that a subclass is ignoring the contract.

Throwing UnsupportedOperationException is the most obvious violation. If a parent class TaskRepository defines a delete() method, and we create an ImmutableTaskRepository subclass that throws an UnsupportedOperationException for delete(), then the subclass is not substitutable. Client code expects the parent’s contract to be honored and will fail at runtime.

A more subtle violation is the “do nothing” implementation. A subclass might implement a method but leave the body empty. If a client calls save(), the postcondition “data is saved” is expected. If the subclass does nothing, the program continues running, but its data is now corrupt or missing. The “correctness of the program” has been altered.

Finally, another common issue is changing the expected behavior. A subclass implements a method but changes its meaning. For example, a TaskService has a getTasks() method that returns all tasks. A FilteredTaskService subclass overrides getTasks() to return only tasks with high priority. A client expecting all tasks will now misbehave, making the subclass non-substitutable. This is the classic LSP violation involving invariants.

5. Conclusion

In this quick lesson, we defined the Liskov Substitution Principle as the rule that child classes must be fully substitutable for their parent classes.

We learned that LSP is enforced in practice through “design by contract”, where subclasses must honor their parent’s preconditions, postconditions, and invariants. Finally, we saw that violations usually come in the form of thrown exceptions, empty methods, or logic that contradicts the parent’s expected behavior.