The Liskov Substitution Principle

In 1987, while delivering a keynote on data abstractions and hierarchies, Barbara Liskov introduced the idea that would eventually become the Liskov substitution principle. The following is a modern (and very formal) description of the principle:

Let Φ(x) be a property provable about objects x of type T. Then Φ(y) should be true for objects y of type S where S is a subtype of T.

When I read the definition for the first time, all I understood was that it's somehow related to inheritance... maybe? I came up with a simpler (albeit less complete) phrase that might be easier to understand:

A piece of code that depends on an object of type P should be able to operate properly with objects of type C, where C is a subtype of P

What this means is that we should design our abstractions and classes in a way that facilitates interoperability across the complete hierarchy. If a subclass overrides the behavior in an unexpected way that breaks compatibility with the rest of the code, we are violating the principle.

Understanding the LSP is much easier with an example. Let's take a look at the Square-Rectangle problem proposed by Robert C. Martin.

Things that work in English might not work in code

We all agree with the notion that 'a square is a rectangle'. This statement is by definition correct, but like a lot of other things, it might not work that well in the world of programming.

Suppose we have the following code where we assert that Rectangle's get_area function behaves properly:

rectangle = #... Gets a Rectangle object from somewhere, maybe a factory
rectangle.set_height(5)
rectangle.set_width(4)
raise "Area calculation is off" unless rectangle.get_area == 20
#... more code 

# In another file, you can find a hypothetical implementation of the Rectangle class
class Rectangle
    def initialize()
    end

    def set_width(width)
        @width = width
    end

    def set_height(height)
        @height = height
    end

    def get_area
        return @width * @height
    end
end

This code will raise an exception unless the get_area calculation returns the proper value.

Now, imagine we implement a Square class, which is also by definition a Rectangle. You can't set the width and height of a square independently, those two values are always the same for a square.

In addition to the public interface of Rectangle, Square implements the set_side method.


rectangle = #... This line now gives a Square object
# The rest of the code stays the same

# In another file, you can find a hypothetical implementation of the Square class
class Square < Rectangle
    def set_width(width)
        set_side(width)
    end
    
    def set_height(height)
        set_side(height)
    end
    
    def set_side(side_length)
        @height = side_length
        @width = side_length
    end

end

If we run this code, the assertion will fail, as the area is now 16. The code that worked with objects of type Rectangle (and its subtypes) breaks with Square, even if it implements the same public interface and behaves as a Square should.

LSP is about semantics

The LSP is about semantic consistency in a type hierarchy. It's not enough to implement the same public interface across said hierarchies, the behavior should also be consistent.

We aim for interoperability, and the ability to work with subtypes without the need for 'special handling' for outliers.

In our example, we might be tempted to solve the problem with an if statement based on types: if we find an object of type Square, perform X different action. This is a problem that can spiral out of control very easily and pollute pieces of code that depend on Rectangle as our code grows.

What we need in place is a disciplined approach: every type that implements the Rectangle public interface should act like a Rectangle as long as we are using the same public interface.

This principle has a broader application than just traditional class hierarchies in statically typed languages. It's also used when sharing a public interface with duck types, or designing microservices with a shared REST interface.

Cool! Now you also know about the L of the S.O.L.I.D principles. Knowledge of this topic is important for making the right architectural choices. I hope you'll be able to use it in your future projects.

What to do next

  • Share this article with friends and colleagues. Thank you for helping me reach people who might find this information useful.
  • You can find the source code with examples for the article in this github repo.
  • Read Barbara Liskov's article on the LSP.
  • There's more info about the OCP in chapter 9 of Clean Architecture. This and other very helpful books are in the recommended reading list.
  • Send me an email with questions, comments or suggestions (it's in the About Me page). Come on, don't be shy!